commit 5e5b5677822e3247703bf5f509b71f511d709871
parent 7c093b4fb0f7d889ccab10eb711823fb6bc5dc3e
Author: Adomas Venčkauskas <adomas.ven@gmail.com>
Date: Wed, 28 Mar 2018 16:22:52 +0300
Add a connector document integration endpoint
Specifically for google docs via the connector, but could potentially be
used for any integration via HTTP or connector.
Diffstat:
10 files changed, 2049 insertions(+), 1706 deletions(-)
diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js
@@ -147,26 +147,33 @@ var Zotero_File_Interface_Bibliography = new function() {
let dialog = document.getElementById("zotero-doc-prefs-dialog");
dialog.setAttribute('title', `${Zotero.clientName} - ${dialog.getAttribute('title')}`);
- if(_io.fieldType == "Bookmark") document.getElementById("formatUsing").selectedIndex = 1;
- var formatOption = (_io.primaryFieldType == "ReferenceMark" ? "referenceMarks" : "fields");
- document.getElementById("fields").label =
- Zotero.getString("integration."+formatOption+".label");
- document.getElementById("fields-caption").textContent =
- Zotero.getString("integration."+formatOption+".caption");
- document.getElementById("fields-file-format-notice").textContent =
- Zotero.getString("integration."+formatOption+".fileFormatNotice");
- document.getElementById("bookmarks-file-format-notice").textContent =
- Zotero.getString("integration.fields.fileFormatNotice");
-
-
- if(_io.automaticJournalAbbreviations === undefined) {
- _io.automaticJournalAbbreviations = Zotero.Prefs.get("cite.automaticJournalAbbreviations");
+ if (document.getElementById("formatUsing-groupbox")) {
+ if (["Field", "ReferenceMark"].includes(_io.primaryFieldType)) {
+ if(_io.fieldType == "Bookmark") document.getElementById("formatUsing").selectedIndex = 1;
+ var formatOption = (_io.primaryFieldType == "ReferenceMark" ? "referenceMarks" : "fields");
+ document.getElementById("fields").label =
+ Zotero.getString("integration."+formatOption+".label");
+ document.getElementById("fields-caption").textContent =
+ Zotero.getString("integration."+formatOption+".caption");
+ document.getElementById("fields-file-format-notice").textContent =
+ Zotero.getString("integration."+formatOption+".fileFormatNotice");
+ document.getElementById("bookmarks-file-format-notice").textContent =
+ Zotero.getString("integration.fields.fileFormatNotice");
+ } else {
+ document.getElementById("formatUsing-groupbox").style.display = "none";
+ _io.fieldType = _io.primaryFieldType;
+ }
}
- if(_io.automaticJournalAbbreviations) {
- document.getElementById("automaticJournalAbbreviations-checkbox").checked = true;
+ if(document.getElementById("automaticJournalAbbreviations-checkbox")) {
+ if(_io.automaticJournalAbbreviations === undefined) {
+ _io.automaticJournalAbbreviations = Zotero.Prefs.get("cite.automaticJournalAbbreviations");
+ }
+ if(_io.automaticJournalAbbreviations) {
+ document.getElementById("automaticJournalAbbreviations-checkbox").checked = true;
+ }
+
+ document.getElementById("automaticCitationUpdates-checkbox").checked = !_io.delayCitationUpdates;
}
-
- document.getElementById("automaticCitationUpdates-checkbox").checked = !_io.delayCitationUpdates;
}
// set style to false, in case this is cancelled
@@ -204,7 +211,8 @@ var Zotero_File_Interface_Bibliography = new function() {
if (isDocPrefs) {
// update status of displayAs box based on style class
var isNote = selectedStyleObj.class == "note";
- document.getElementById("displayAs-groupbox").hidden = !isNote;
+ var multipleNotesSupported = _io.supportedNotes.length > 1;
+ document.getElementById("displayAs-groupbox").hidden = !isNote || !multipleNotesSupported;
// update status of formatUsing box based on style class
if(isNote) document.getElementById("formatUsing").selectedIndex = 0;
diff --git a/chrome/content/zotero/integration/integrationDocPrefs.xul b/chrome/content/zotero/integration/integrationDocPrefs.xul
@@ -68,7 +68,7 @@
</radiogroup>
</groupbox>
- <groupbox>
+ <groupbox id="formatUsing-groupbox">
<caption label="&zotero.integration.prefs.formatUsing.label;"/>
<radiogroup id="formatUsing" orient="vertical">
diff --git a/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js b/chrome/content/zotero/xpcom/connector/httpIntegrationClient.js
@@ -0,0 +1,194 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2017 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see <http://www.gnu.org/licenses/>.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+/**
+ * This is a HTTP-based integration interface for Zotero. The actual
+ * heavy lifting occurs in the connector and/or wherever the connector delegates the heavy
+ * lifting to.
+ */
+Zotero.HTTPIntegrationClient = {
+ deferredResponse: null,
+ sendCommandPromise: Zotero.Promise.resolve(),
+ sendCommand: async function(command, args=[]) {
+ let payload = JSON.stringify({command, arguments: args});
+ function sendCommand() {
+ Zotero.HTTPIntegrationClient.deferredResponse = Zotero.Promise.defer();
+ Zotero.HTTPIntegrationClient.sendResponse.apply(Zotero.HTTPIntegrationClient,
+ [200, 'application/json', payload]);
+ return Zotero.HTTPIntegrationClient.deferredResponse.promise;
+ }
+ // Force issued commands to occur sequentially, since these are really just
+ // a sequence of HTTP requests and responses.
+ // We might want to consider something better later, but this has the advantage of
+ // being easy to interface with as a Client, as you don't need SSE or WS.
+ if (command != 'Document.complete') {
+ Zotero.HTTPIntegrationClient.sendCommandPromise =
+ Zotero.HTTPIntegrationClient.sendCommandPromise.then(sendCommand, sendCommand);
+ } else {
+ await Zotero.HTTPIntegrationClient.sendCommandPromise;
+ sendCommand();
+ }
+ return Zotero.HTTPIntegrationClient.sendCommandPromise;
+ }
+};
+
+Zotero.HTTPIntegrationClient.Application = function() {
+ this.primaryFieldType = "Http";
+ this.secondaryFieldType = "Http";
+ this.outputFormat = 'html';
+ this.supportedNotes = ['footnotes'];
+};
+Zotero.HTTPIntegrationClient.Application.prototype = {
+ getActiveDocument: async function() {
+ let result = await Zotero.HTTPIntegrationClient.sendCommand('Application.getActiveDocument');
+ this.outputFormat = result.outputFormat || this.outputFormat;
+ this.supportedNotes = result.supportedNotes || this.supportedNotes;
+ return new Zotero.HTTPIntegrationClient.Document(result.documentID);
+ }
+};
+
+/**
+ * See integrationTests.js
+ */
+Zotero.HTTPIntegrationClient.Document = function(documentID) {
+ this._documentID = documentID;
+};
+for (let method of ["activate", "canInsertField", "displayAlert", "getDocumentData",
+ "setDocumentData", "setBibliographyStyle"]) {
+ Zotero.HTTPIntegrationClient.Document.prototype[method] = async function() {
+ return Zotero.HTTPIntegrationClient.sendCommand("Document."+method,
+ [this._documentID].concat(Array.prototype.slice.call(arguments)));
+ };
+}
+
+// @NOTE Currently unused, prompts are done using the connector
+Zotero.HTTPIntegrationClient.Document.prototype._displayAlert = async function(dialogText, icon, buttons) {
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_OK)
+ + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
+
+ switch (buttons) {
+ case DIALOG_BUTTONS_OK:
+ buttonFlags = ps.BUTTON_POS_0_DEFAULT + ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK; break;
+ case DIALOG_BUTTONS_OK_CANCEL:
+ buttonFlags = ps.BUTTON_POS_0_DEFAULT + ps.STD_OK_CANCEL_BUTTONS; break;
+ case DIALOG_BUTTONS_YES_NO:
+ buttonFlags = ps.BUTTON_POS_0_DEFAULT + ps.STD_YES_NO_BUTTONS; break;
+ case DIALOG_BUTTONS_YES_NO_CANCEL:
+ buttonFlags = ps.BUTTON_POS_0_DEFAULT + ps.BUTTON_POS_0 * ps.BUTTON_TITLE_YES +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_NO +
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_CANCEL; break;
+ }
+
+ var result = ps.confirmEx(
+ null,
+ "Zotero",
+ dialogText,
+ buttonFlags,
+ null, null, null,
+ null,
+ {}
+ );
+
+ switch (buttons) {
+ default:
+ break;
+ case DIALOG_BUTTONS_OK_CANCEL:
+ case DIALOG_BUTTONS_YES_NO:
+ result = (result+1)%2; break;
+ case DIALOG_BUTTONS_YES_NO_CANCEL:
+ result = result == 0 ? 2 : result == 2 ? 0 : 1; break;
+ }
+ await this.activate();
+ return result;
+}
+Zotero.HTTPIntegrationClient.Document.prototype.cleanup = async function() {};
+Zotero.HTTPIntegrationClient.Document.prototype.cursorInField = async function(fieldType) {
+ var retVal = await Zotero.HTTPIntegrationClient.sendCommand("Document.cursorInField", [this._documentID, fieldType]);
+ if (!retVal) return null;
+ return new Zotero.HTTPIntegrationClient.Field(this._documentID, retVal);
+};
+Zotero.HTTPIntegrationClient.Document.prototype.insertField = async function(fieldType, noteType) {
+ var retVal = await Zotero.HTTPIntegrationClient.sendCommand("Document.insertField", [this._documentID, fieldType, parseInt(noteType) || 0]);
+ return new Zotero.HTTPIntegrationClient.Field(this._documentID, retVal);
+};
+Zotero.HTTPIntegrationClient.Document.prototype.getFields = async function(fieldType) {
+ var retVal = await Zotero.HTTPIntegrationClient.sendCommand("Document.getFields", [this._documentID, fieldType]);
+ return retVal.map(field => new Zotero.HTTPIntegrationClient.Field(this._documentID, field));
+};
+Zotero.HTTPIntegrationClient.Document.prototype.convert = async function(fields, fieldType, noteTypes) {
+ fields = fields.map((f) => f._id);
+ await Zotero.HTTPIntegrationClient.sendCommand("Field.convert", [this._documentID, fields, fieldType, noteTypes]);
+};
+Zotero.HTTPIntegrationClient.Document.prototype.complete = async function() {
+ Zotero.HTTPIntegrationClient.inProgress = false;
+ Zotero.HTTPIntegrationClient.sendCommand("Document.complete", [this._documentID]);
+};
+
+/**
+ * See integrationTests.js
+ */
+Zotero.HTTPIntegrationClient.Field = function(documentID, json) {
+ this._documentID = documentID;
+ this._id = json.id;
+ this._code = json.code;
+ this._text = json.text;
+ this._noteIndex = json.noteIndex;
+};
+Zotero.HTTPIntegrationClient.Field.prototype = {};
+
+for (let method of ["delete", "select", "removeCode"]) {
+ Zotero.HTTPIntegrationClient.Field.prototype[method] = async function() {
+ return Zotero.HTTPIntegrationClient.sendCommand("Field."+method,
+ [this._documentID, this._id].concat(Array.prototype.slice.call(arguments)));
+ };
+}
+Zotero.HTTPIntegrationClient.Field.prototype.getText = async function() {
+ return this._text;
+};
+Zotero.HTTPIntegrationClient.Field.prototype.setText = async function(text, isRich) {
+ // The HTML will be stripped by Google Docs and and since we're
+ // caching this value, we need to strip it ourselves
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow('navigator:browser');
+ var doc = new win.DOMParser().parseFromString(text, "text/html");
+ this._text = doc.documentElement.textContent;
+ return Zotero.HTTPIntegrationClient.sendCommand("Field.setText", [this._documentID, this._id, text, isRich]);
+};
+Zotero.HTTPIntegrationClient.Field.prototype.getCode = async function() {
+ return this._code;
+};
+Zotero.HTTPIntegrationClient.Field.prototype.setCode = async function(code) {
+ this._code = code;
+ return Zotero.HTTPIntegrationClient.sendCommand("Field.setCode", [this._documentID, this._id, code]);
+};
+Zotero.HTTPIntegrationClient.Field.prototype.getNoteIndex = async function() {
+ return this._noteIndex;
+};
+Zotero.HTTPIntegrationClient.Field.prototype.equals = async function(arg) {
+ return this._id === arg._id;
+};
diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js
@@ -0,0 +1,1274 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2011 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see <http://www.gnu.org/licenses/>.
+
+ ***** END LICENSE BLOCK *****
+*/
+const CONNECTOR_API_VERSION = 2;
+
+Zotero.Server.Connector = {
+ _waitingForSelection: {},
+
+ getSaveTarget: function () {
+ var zp = Zotero.getActiveZoteroPane(),
+ library = null,
+ collection = null,
+ editable = true;
+ try {
+ library = Zotero.Libraries.get(zp.getSelectedLibraryID());
+ collection = zp.getSelectedCollection();
+ editable = zp.collectionsView.editable;
+ }
+ catch (e) {
+ let id = Zotero.Prefs.get('lastViewedFolder');
+ if (id) {
+ ({ library, collection, editable } = this.resolveTarget(id));
+ }
+ }
+
+ // Default to My Library if present if pane not yet opened
+ // (which should never be the case anymore)
+ if (!library) {
+ let userLibrary = Zotero.Libraries.userLibrary;
+ if (userLibrary) {
+ library = userLibrary;
+ }
+ }
+
+ return { library, collection, editable };
+ },
+
+ resolveTarget: function (targetID) {
+ var library;
+ var collection;
+ var editable;
+
+ var type = targetID[0];
+ var id = parseInt(('' + targetID).substr(1));
+
+ switch (type) {
+ case 'L':
+ library = Zotero.Libraries.get(id);
+ editable = library.editable;
+ break;
+
+ case 'C':
+ collection = Zotero.Collections.get(id);
+ library = collection.library;
+ editable = collection.editable;
+ break;
+
+ default:
+ throw new Error(`Unsupported target type '${type}'`);
+ }
+
+ return { library, collection, editable };
+ }
+};
+Zotero.Server.Connector.Data = {};
+
+Zotero.Server.Connector.SessionManager = {
+ _sessions: new Map(),
+
+ get: function (id) {
+ return this._sessions.get(id);
+ },
+
+ create: function (id, action, requestData) {
+ // Legacy connector
+ if (!id) {
+ Zotero.debug("No session id provided by client", 2);
+ 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 = function (id, action, requestData) {
+ this.id = id;
+ this.created = new Date();
+ this._action = action;
+ this._requestData = requestData;
+ this._items = new Set();
+};
+
+Zotero.Server.Connector.SaveSession.prototype.addItem = async function (item) {
+ return this.addItems([item]);
+};
+
+Zotero.Server.Connector.SaveSession.prototype.addItems = async function (items) {
+ for (let item of items) {
+ this._items.add(item);
+ }
+
+ // Update the items with the current target data, in case it changed since the save began
+ await this._updateItems(items);
+};
+
+/**
+ * Change the target data for this session and update any items that have already been saved
+ */
+Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID, tags) {
+ var previousTargetID = this._currentTargetID;
+ this._currentTargetID = targetID;
+ this._currentTags = tags || "";
+
+ // Select new destination in collections pane
+ var win = Zotero.getActiveZoteroPane();
+ if (win && win.collectionsView) {
+ await win.collectionsView.selectByID(targetID);
+ }
+ // If window is closed, select target collection re-open
+ else {
+ Zotero.Prefs.set('lastViewedFolder', targetID);
+ }
+
+ // If moving from a non-filesEditable library to a filesEditable library, resave from
+ // original data, since there might be files that weren't saved or were removed
+ if (previousTargetID && previousTargetID != targetID) {
+ let { library: oldLibrary } = Zotero.Server.Connector.resolveTarget(previousTargetID);
+ let { library: newLibrary } = Zotero.Server.Connector.resolveTarget(targetID);
+ if (oldLibrary != newLibrary && !oldLibrary.filesEditable && newLibrary.filesEditable) {
+ Zotero.debug("Resaving items to filesEditable library");
+ if (this._action == 'saveItems' || this._action == 'saveSnapshot') {
+ // Delete old items
+ for (let item of this._items) {
+ await item.eraseTx();
+ }
+ let actionUC = Zotero.Utilities.capitalize(this._action);
+ let newItems = await Zotero.Server.Connector[actionUC].prototype[this._action](
+ targetID, this._requestData
+ );
+ // saveSnapshot only returns a single item
+ if (this._action == 'saveSnapshot') {
+ newItems = [newItems];
+ }
+ this._items = new Set(newItems);
+ }
+ }
+ }
+
+ await this._updateItems(this._items);
+
+ // If a single item was saved, select it (or its parent, if it now has one)
+ if (win && win.collectionsView && this._items.size == 1) {
+ let item = Array.from(this._items)[0];
+ item = item.isTopLevelItem() ? item : item.parentItem;
+ // Don't select if in trash
+ if (!item.deleted) {
+ await win.selectItem(item.id);
+ }
+ }
+};
+
+/**
+ * Update the passed items with the current target and tags
+ */
+Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async function (items) {
+ if (items.length == 0) {
+ return;
+ }
+
+ var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
+ var libraryID = library.libraryID;
+
+ var tags = this._currentTags.trim();
+ tags = tags ? tags.split(/\s*,\s*/) : [];
+
+ Zotero.debug("Updating items for connector save session " + this.id);
+
+ for (let item of items) {
+ let newLibrary = Zotero.Libraries.get(library.libraryID);
+
+ if (item.libraryID != libraryID) {
+ let newItem = await item.moveToLibrary(libraryID);
+ // Replace item in session
+ this._items.delete(item);
+ this._items.add(newItem);
+ }
+
+ // If the item is now a child item (e.g., from Retrieve Metadata for PDF), 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;
+ }
+ // 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();
+});
+
+
+Zotero.Server.Connector.SaveSession.prototype._updateRecents = function () {
+ 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');
+ }
+};
+
+
+Zotero.Server.Connector.AttachmentProgressManager = new function() {
+ var attachmentsInProgress = new WeakMap(),
+ attachmentProgress = {},
+ id = 1;
+
+ /**
+ * Adds attachments to attachment progress manager
+ */
+ this.add = function(attachments) {
+ for(var i=0; i<attachments.length; i++) {
+ var attachment = attachments[i];
+ attachmentsInProgress.set(attachment, (attachment.id = id++));
+ }
+ };
+
+ /**
+ * Called on attachment progress
+ */
+ this.onProgress = function(attachment, progress, error) {
+ attachmentProgress[attachmentsInProgress.get(attachment)] = progress;
+ };
+
+ /**
+ * Gets progress for a given progressID
+ */
+ this.getProgressForID = function(progressID) {
+ return progressID in attachmentProgress ? attachmentProgress[progressID] : 0;
+ };
+
+ /**
+ * Check if we have received progress for a given attachment
+ */
+ this.has = function(attachment) {
+ return attachmentsInProgress.has(attachment)
+ && attachmentsInProgress.get(attachment) in attachmentProgress;
+ }
+};
+
+/**
+ * Lists all available translators, including code for translators that should be run on every page
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * Array of Zotero.Translator objects
+ */
+Zotero.Server.Connector.GetTranslators = function() {};
+Zotero.Server.Endpoints["/connector/getTranslators"] = Zotero.Server.Connector.GetTranslators;
+Zotero.Server.Connector.GetTranslators.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Gets available translator list and other important data
+ * @param {Object} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(data, sendResponseCallback) {
+ // Translator data
+ var me = this;
+ if(data.url) {
+ Zotero.Translators.getWebTranslatorsForLocation(data.url, data.rootUrl).then(function(data) {
+ sendResponseCallback(200, "application/json",
+ JSON.stringify(me._serializeTranslators(data[0])));
+ });
+ } else {
+ Zotero.Translators.getAll().then(function(translators) {
+ var responseData = me._serializeTranslators(translators);
+ sendResponseCallback(200, "application/json", JSON.stringify(responseData));
+ }).catch(function(e) {
+ sendResponseCallback(500);
+ throw e;
+ }).done();
+ }
+ },
+
+ _serializeTranslators: function(translators) {
+ var responseData = [];
+ let properties = ["translatorID", "translatorType", "label", "creator", "target", "targetAll",
+ "minVersion", "maxVersion", "priority", "browserSupport", "inRepository", "lastUpdated"];
+ for (var translator of translators) {
+ responseData.push(translator.serialize(properties));
+ }
+ return responseData;
+ }
+}
+
+/**
+ * Detects whether there is an available translator to handle a given page
+ *
+ * Accepts:
+ * uri - The URI of the page to be saved
+ * html - document.innerHTML or equivalent
+ * cookie - document.cookie or equivalent
+ *
+ * Returns a list of available translators as an array
+ */
+Zotero.Server.Connector.Detect = function() {};
+Zotero.Server.Endpoints["/connector/detect"] = Zotero.Server.Connector.Detect;
+Zotero.Server.Connector.Detect.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Loads HTML into a hidden browser and initiates translator detection
+ * @param {Object} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(url, data, sendResponseCallback) {
+ this.sendResponse = sendResponseCallback;
+ this._parsedPostData = data;
+
+ this._translate = new Zotero.Translate("web");
+ this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) });
+
+ Zotero.Server.Connector.Data[this._parsedPostData["uri"]] = "<html>"+this._parsedPostData["html"]+"</html>";
+ this._browser = Zotero.Browser.createHiddenBrowser();
+
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null);
+
+ var pageShowCalled = false;
+ var me = this;
+ this._translate.setCookieSandbox(new Zotero.CookieSandbox(this._browser,
+ this._parsedPostData["uri"], this._parsedPostData["cookie"], url.userAgent));
+ this._browser.addEventListener("DOMContentLoaded", function() {
+ try {
+ if(me._browser.contentDocument.location.href == "about:blank") return;
+ if(pageShowCalled) return;
+ pageShowCalled = true;
+ delete Zotero.Server.Connector.Data[me._parsedPostData["uri"]];
+
+ // get translators
+ me._translate.setDocument(me._browser.contentDocument);
+ me._translate.setLocation(me._parsedPostData["uri"], me._parsedPostData["uri"]);
+ me._translate.getTranslators();
+ } catch(e) {
+ sendResponseCallback(500);
+ throw e;
+ }
+ }, false);
+
+ me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"]));
+ },
+
+ /**
+ * Callback to be executed when list of translators becomes available. Sends standard
+ * translator passing properties with proxies where available for translators.
+ * @param {Zotero.Translate} translate
+ * @param {Zotero.Translator[]} translators
+ */
+ _translatorsAvailable: function(translate, translators) {
+ translators = translators.map(function(translator) {
+ translator = translator.serialize(TRANSLATOR_PASSING_PROPERTIES.concat('proxy'));
+ translator.proxy = translator.proxy ? translator.proxy.toJSON() : null;
+ return translator;
+ });
+ this.sendResponse(200, "application/json", JSON.stringify(translators));
+
+ Zotero.Browser.deleteHiddenBrowser(this._browser);
+ }
+}
+
+/**
+ * Performs translation of a given page
+ *
+ * Accepts:
+ * uri - The URI of the page to be saved
+ * html - document.innerHTML or equivalent
+ * cookie - document.cookie or equivalent
+ * translatorID [optional] - a translator ID as returned by /connector/detect
+ *
+ * Returns:
+ * If a single item, sends response code 201 with item in body.
+ * If multiple items, sends response code 300 with the following content:
+ * items - list of items in the format typically passed to the selectItems handler
+ * instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call
+ * uri - the URI of the page for which multiple items are available
+ */
+Zotero.Server.Connector.SavePage = function() {};
+Zotero.Server.Endpoints["/connector/savePage"] = Zotero.Server.Connector.SavePage;
+Zotero.Server.Connector.SavePage.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Either loads HTML into a hidden browser and initiates translation, or saves items directly
+ * to the database
+ * @param {Object} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(url, data, sendResponseCallback) {
+ this.sendResponse = sendResponseCallback;
+ Zotero.Server.Connector.Detect.prototype.init.apply(this, [url, data, sendResponseCallback])
+ },
+
+ /**
+ * Callback to be executed when items must be selected
+ * @param {Zotero.Translate} translate
+ * @param {Object} itemList ID=>text pairs representing available items
+ */
+ _selectItems: function(translate, itemList, callback) {
+ var instanceID = Zotero.randomString();
+ Zotero.Server.Connector._waitingForSelection[instanceID] = this;
+
+ // Fix for translators that don't create item lists as objects
+ if(itemList.push && typeof itemList.push === "function") {
+ var newItemList = {};
+ for(var item in itemList) {
+ newItemList[item] = itemList[item];
+ }
+ itemList = newItemList;
+ }
+
+ // Send "Multiple Choices" HTTP response
+ this.sendResponse(300, "application/json", JSON.stringify({selectItems: itemList, instanceID: instanceID, uri: this._parsedPostData.uri}));
+ this.selectedItemsCallback = callback;
+ },
+
+ /**
+ * Callback to be executed when list of translators becomes available. Opens progress window,
+ * selects specified translator, and initiates translation.
+ * @param {Zotero.Translate} translate
+ * @param {Zotero.Translator[]} translators
+ */
+ _translatorsAvailable: function(translate, translators) {
+ // make sure translatorsAvailable succeded
+ if(!translators.length) {
+ Zotero.Browser.deleteHiddenBrowser(this._browser);
+ this.sendResponse(500);
+ return;
+ }
+
+ var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
+ var libraryID = library.libraryID;
+
+ // set handlers for translation
+ var me = this;
+ var jsonItems = [];
+ translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) });
+ translate.setHandler("itemDone", function(obj, item, jsonItem) {
+ if(collection) {
+ collection.addItem(item.id);
+ }
+ Zotero.Server.Connector.AttachmentProgressManager.add(jsonItem.attachments);
+
+ jsonItems.push(jsonItem);
+ });
+ translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) {
+ Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error);
+ });
+ translate.setHandler("done", function(obj, item) {
+ Zotero.Browser.deleteHiddenBrowser(me._browser);
+ if(jsonItems.length || me.selectedItems === false) {
+ me.sendResponse(201, "application/json", JSON.stringify({items: jsonItems}));
+ } else {
+ me.sendResponse(500);
+ }
+ });
+
+ if (this._parsedPostData.translatorID) {
+ translate.setTranslator(this._parsedPostData.translatorID);
+ } else {
+ translate.setTranslator(translators[0]);
+ }
+ translate.translate(libraryID);
+ }
+}
+
+/**
+ * Saves items to DB
+ *
+ * Accepts:
+ * items - an array of JSON format items
+ * Returns:
+ * 201 response code with item in body.
+ */
+Zotero.Server.Connector.SaveItems = function() {};
+Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItems;
+Zotero.Server.Connector.SaveItems.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Either loads HTML into a hidden browser and initiates translation, or saves items directly
+ * to the database
+ */
+ init: Zotero.Promise.coroutine(function* (requestData) {
+ var data = requestData.data;
+
+ var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
+ var libraryID = library.libraryID;
+ var targetID = collection ? collection.treeViewID : library.treeViewID;
+
+ try {
+ var session = Zotero.Server.Connector.SessionManager.create(
+ data.sessionID,
+ 'saveItems',
+ requestData
+ );
+ }
+ catch (e) {
+ return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
+ }
+ yield session.update(targetID);
+
+ // TODO: Default to My Library root, since it's changeable
+ if (!library.editable) {
+ Zotero.logError("Can't add item to read-only library " + library.name);
+ return [500, "application/json", JSON.stringify({ libraryEditable: false })];
+ }
+
+ return new Zotero.Promise((resolve) => {
+ try {
+ this.saveItems(
+ targetID,
+ requestData,
+ function (topLevelItems) {
+ resolve([201, "application/json", JSON.stringify({items: topLevelItems})]);
+ }
+ )
+ // Add items to session once all attachments have been saved
+ .then(function (items) {
+ session.addItems(items);
+ });
+ }
+ catch (e) {
+ Zotero.logError(e);
+ resolve(500);
+ }
+ });
+ }),
+
+ saveItems: async function (target, requestData, onTopLevelItemsDone) {
+ var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
+
+ var data = requestData.data;
+ var cookieSandbox = data.uri
+ ? new Zotero.CookieSandbox(
+ null,
+ data.uri,
+ data.detailedCookies ? "" : data.cookie || "",
+ requestData.headers["User-Agent"]
+ )
+ : null;
+ if (cookieSandbox && data.detailedCookies) {
+ cookieSandbox.addCookiesFromHeader(data.detailedCookies);
+ }
+
+ for (let item of data.items) {
+ Zotero.Server.Connector.AttachmentProgressManager.add(item.attachments);
+ }
+
+ var proxy = data.proxy && new Zotero.Proxy(data.proxy);
+
+ // Save items
+ var itemSaver = new Zotero.Translate.ItemSaver({
+ libraryID: library.libraryID,
+ collections: collection ? [collection.id] : undefined,
+ attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD,
+ forceTagType: 1,
+ referrer: data.uri,
+ cookieSandbox,
+ proxy
+ });
+ return itemSaver.saveItems(
+ data.items,
+ Zotero.Server.Connector.AttachmentProgressManager.onProgress,
+ function () {
+ // Remove attachments from item.attachments that aren't being saved. We have to
+ // clone the items so that we don't mutate the data stored in the session.
+ var savedItems = [...data.items.map(item => Object.assign({}, item))];
+ for (let item of savedItems) {
+ item.attachments = item.attachments
+ .filter(attachment => {
+ return Zotero.Server.Connector.AttachmentProgressManager.has(attachment);
+ });
+ }
+ if (onTopLevelItemsDone) {
+ onTopLevelItemsDone(savedItems);
+ }
+ }
+ );
+ }
+}
+
+/**
+ * Saves a snapshot to the DB
+ *
+ * Accepts:
+ * uri - The URI of the page to be saved
+ * html - document.innerHTML or equivalent
+ * cookie - document.cookie or equivalent
+ * Returns:
+ * Nothing (200 OK response)
+ */
+Zotero.Server.Connector.SaveSnapshot = function() {};
+Zotero.Server.Endpoints["/connector/saveSnapshot"] = Zotero.Server.Connector.SaveSnapshot;
+Zotero.Server.Connector.SaveSnapshot.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Save snapshot
+ */
+ init: async function (requestData) {
+ var data = requestData.data;
+
+ var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
+ var targetID = collection ? collection.treeViewID : library.treeViewID;
+
+ try {
+ var session = Zotero.Server.Connector.SessionManager.create(
+ data.sessionID,
+ 'saveSnapshot',
+ requestData
+ );
+ }
+ catch (e) {
+ return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
+ }
+ await session.update(collection ? collection.treeViewID : library.treeViewID);
+
+ // TODO: Default to My Library root, since it's changeable
+ if (!library.editable) {
+ Zotero.logError("Can't add item to read-only library " + library.name);
+ return [500, "application/json", JSON.stringify({ libraryEditable: false })];
+ }
+
+ try {
+ let item = await this.saveSnapshot(targetID, requestData);
+ await session.addItem(item);
+ }
+ catch (e) {
+ Zotero.logError(e);
+ return 500;
+ }
+
+ return 201;
+ },
+
+ saveSnapshot: async function (target, requestData) {
+ var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
+ var libraryID = library.libraryID;
+ var data = requestData.data;
+
+ var cookieSandbox = data.url
+ ? new Zotero.CookieSandbox(
+ null,
+ data.url,
+ data.detailedCookies ? "" : data.cookie || "",
+ requestData.headers["User-Agent"]
+ )
+ : null;
+ if (cookieSandbox && data.detailedCookies) {
+ cookieSandbox.addCookiesFromHeader(data.detailedCookies);
+ }
+
+ if (data.pdf && library.filesEditable) {
+ let item = await Zotero.Attachments.importFromURL({
+ libraryID,
+ url: data.url,
+ collections: collection ? [collection.id] : undefined,
+ contentType: "application/pdf",
+ cookieSandbox
+ });
+
+ // Automatically recognize PDF
+ Zotero.RecognizePDF.autoRecognizeItems([item]);
+
+ return item;
+ }
+
+ return new Zotero.Promise((resolve, reject) => {
+ Zotero.Server.Connector.Data[data.url] = "<html>" + data.html + "</html>";
+ Zotero.HTTP.loadDocuments(
+ ["zotero://connector/" + encodeURIComponent(data.url)],
+ async function (doc) {
+ delete Zotero.Server.Connector.Data[data.url];
+
+ try {
+ // Create new webpage item
+ let item = new Zotero.Item("webpage");
+ item.libraryID = libraryID;
+ item.setField("title", doc.title);
+ item.setField("url", data.url);
+ item.setField("accessDate", "CURRENT_TIMESTAMP");
+ if (collection) {
+ item.setCollections([collection.id]);
+ }
+ var itemID = await item.saveTx();
+
+ // Save snapshot
+ if (library.filesEditable && !data.skipSnapshot) {
+ await Zotero.Attachments.importFromDocument({
+ document: doc,
+ parentItemID: itemID
+ });
+ }
+
+ resolve(item);
+ }
+ catch (e) {
+ reject(e);
+ }
+ },
+ null,
+ null,
+ false,
+ cookieSandbox
+ );
+ });
+ }
+}
+
+/**
+ * Handle item selection
+ *
+ * Accepts:
+ * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler
+ * instanceID - as returned by savePage call
+ * Returns:
+ * 201 response code with empty body
+ */
+Zotero.Server.Connector.SelectItems = function() {};
+Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems;
+Zotero.Server.Connector.SelectItems.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Finishes up translation when item selection is complete
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(data, sendResponseCallback) {
+ var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID];
+ saveInstance.sendResponse = sendResponseCallback;
+
+ var selectedItems = false;
+ for(var i in data.selectedItems) {
+ selectedItems = data.selectedItems;
+ break;
+ }
+ saveInstance.selectedItemsCallback(selectedItems);
+ }
+}
+
+/**
+ *
+ *
+ * Accepts:
+ * sessionID - A session ID previously passed to /saveItems
+ * target - A treeViewID (L1, C23, etc.) for the library or collection to save to
+ * tags - A string of tags separated by commas
+ *
+ * Returns:
+ * 200 response on successful change
+ * 400 on error with 'error' property in JSON
+ */
+Zotero.Server.Connector.UpdateSession = function() {};
+Zotero.Server.Endpoints["/connector/updateSession"] = Zotero.Server.Connector.UpdateSession;
+Zotero.Server.Connector.UpdateSession.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ init: async function (requestData) {
+ var data = requestData.data
+
+ if (!data.sessionID) {
+ return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
+ }
+
+ var session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
+ if (!session) {
+ Zotero.debug("Can't find session " + data.sessionID, 1);
+ return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
+ }
+
+ // Parse treeViewID
+ var [type, id] = [data.target[0], parseInt(data.target.substr(1))];
+ var tags = data.tags;
+
+ if (type == 'C') {
+ let collection = await Zotero.Collections.getAsync(id);
+ if (!collection) {
+ return [400, "application/json", JSON.stringify({ error: "COLLECTION_NOT_FOUND" })];
+ }
+ }
+
+ await session.update(data.target, tags);
+
+ return [200, "application/json", JSON.stringify({})];
+ }
+};
+
+Zotero.Server.Connector.DelaySync = function () {};
+Zotero.Server.Endpoints["/connector/delaySync"] = Zotero.Server.Connector.DelaySync;
+Zotero.Server.Connector.DelaySync.prototype = {
+ supportedMethods: ["POST"],
+
+ init: async function (requestData) {
+ Zotero.Sync.Runner.delaySync(10000);
+ return [204];
+ }
+};
+
+/**
+ * Gets progress for an attachment that is currently being saved
+ *
+ * Accepts:
+ * Array of attachment IDs returned by savePage, saveItems, or saveSnapshot
+ * Returns:
+ * 200 response code with current progress in body. Progress is either a number
+ * between 0 and 100 or "false" to indicate that saving failed.
+ */
+Zotero.Server.Connector.Progress = function() {};
+Zotero.Server.Endpoints["/connector/attachmentProgress"] = Zotero.Server.Connector.Progress;
+Zotero.Server.Connector.Progress.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(data, sendResponseCallback) {
+ sendResponseCallback(200, "application/json",
+ JSON.stringify(data.map(id => Zotero.Server.Connector.AttachmentProgressManager.getProgressForID(id))));
+ }
+};
+
+/**
+ * Translates resources using import translators
+ *
+ * Returns:
+ * - Object[Item] an array of imported items
+ */
+
+Zotero.Server.Connector.Import = function() {};
+Zotero.Server.Endpoints["/connector/import"] = Zotero.Server.Connector.Import;
+Zotero.Server.Connector.Import.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: '*',
+ permitBookmarklet: false,
+
+ init: async function (requestData) {
+ let translate = new Zotero.Translate.Import();
+ translate.setString(requestData.data);
+ let translators = await translate.getTranslators();
+ if (!translators || !translators.length) {
+ return 400;
+ }
+ translate.setTranslator(translators[0]);
+ var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
+ var libraryID = library.libraryID;
+ if (!library.editable) {
+ Zotero.logError("Can't import into read-only library " + library.name);
+ return [500, "application/json", JSON.stringify({ libraryEditable: false })];
+ }
+
+ try {
+ var session = Zotero.Server.Connector.SessionManager.create(requestData.query.session);
+ }
+ catch (e) {
+ return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
+ }
+ await session.update(collection ? collection.treeViewID : library.treeViewID);
+
+ let items = await translate.translate({
+ libraryID,
+ collections: collection ? [collection.id] : null,
+ // Import translation skips selection by default, so force it to occur
+ saveOptions: {
+ skipSelect: false
+ }
+ });
+ session.addItems(items);
+
+ return [201, "application/json", JSON.stringify(items)];
+ }
+}
+
+/**
+ * Install CSL styles
+ *
+ * Returns:
+ * - {name: styleName}
+ */
+
+Zotero.Server.Connector.InstallStyle = function() {};
+Zotero.Server.Endpoints["/connector/installStyle"] = Zotero.Server.Connector.InstallStyle;
+Zotero.Server.Connector.InstallStyle.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: '*',
+ permitBookmarklet: false,
+
+ init: Zotero.Promise.coroutine(function* (requestData) {
+ try {
+ var styleName = yield Zotero.Styles.install(
+ requestData.data, requestData.query.origin || null, true
+ );
+ } catch (e) {
+ return [400, "text/plain", e.message];
+ }
+ return [201, "application/json", JSON.stringify({name: styleName})];
+ })
+};
+
+/**
+ * Get code for a translator
+ *
+ * Accepts:
+ * translatorID
+ * Returns:
+ * code - translator code
+ */
+Zotero.Server.Connector.GetTranslatorCode = function() {};
+Zotero.Server.Endpoints["/connector/getTranslatorCode"] = Zotero.Server.Connector.GetTranslatorCode;
+Zotero.Server.Connector.GetTranslatorCode.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Returns a 200 response to say the server is alive
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(postData, sendResponseCallback) {
+ var translator = Zotero.Translators.get(postData.translatorID);
+ translator.getCode().then(function(code) {
+ sendResponseCallback(200, "application/javascript", code);
+ });
+ }
+}
+
+/**
+ * Get selected collection
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * libraryID
+ * libraryName
+ * collectionID
+ * collectionName
+ */
+Zotero.Server.Connector.GetSelectedCollection = function() {};
+Zotero.Server.Endpoints["/connector/getSelectedCollection"] = Zotero.Server.Connector.GetSelectedCollection;
+Zotero.Server.Connector.GetSelectedCollection.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ /**
+ * Returns a 200 response to say the server is alive
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(postData, sendResponseCallback) {
+ var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
+ var response = {
+ libraryID: library.libraryID,
+ libraryName: library.name,
+ libraryEditable: library.editable,
+ editable
+ };
+
+ if(collection && collection.id) {
+ response.id = collection.id;
+ response.name = collection.name;
+ } else {
+ response.id = null;
+ response.name = response.libraryName;
+ }
+
+ // Get list of editable libraries and collections
+ var collections = [];
+ var originalLibraryID = library.libraryID;
+ for (let library of Zotero.Libraries.getAll()) {
+ if (!library.editable) continue;
+
+ // Add recent: true for recent targets
+
+ collections.push(
+ {
+ id: library.treeViewID,
+ name: library.name
+ },
+ ...Zotero.Collections.getByLibrary(library.libraryID, true).map(c => ({
+ id: c.treeViewID,
+ name: c.name,
+ level: c.level + 1 || 1 // Added by Zotero.Collections._getByContainer()
+ }))
+ );
+ }
+ response.targets = collections;
+
+ // Mark recent targets
+ try {
+ let recents = Zotero.Prefs.get('recentSaveTargets');
+ if (recents) {
+ recents = new Set(JSON.parse(recents).map(o => o.id));
+ for (let target of response.targets) {
+ if (recents.has(target.id)) {
+ target.recent = true;
+ }
+ }
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.Prefs.clear('recentSaveTargets');
+ }
+
+ // TODO: Limit debug size
+ sendResponseCallback(200, "application/json", JSON.stringify(response));
+ }
+}
+
+/**
+ * Get a list of client hostnames (reverse local IP DNS)
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * {Array} hostnames
+ */
+Zotero.Server.Connector.GetClientHostnames = {};
+Zotero.Server.Connector.GetClientHostnames = function() {};
+Zotero.Server.Endpoints["/connector/getClientHostnames"] = Zotero.Server.Connector.GetClientHostnames;
+Zotero.Server.Connector.GetClientHostnames.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: false,
+
+ /**
+ * Returns a 200 response to say the server is alive
+ */
+ init: Zotero.Promise.coroutine(function* (requestData) {
+ try {
+ var hostnames = yield Zotero.Proxies.DNS.getHostnames();
+ } catch(e) {
+ return 500;
+ }
+ return [200, "application/json", JSON.stringify(hostnames)];
+ })
+};
+
+/**
+ * Get a list of stored proxies
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * {Array} hostnames
+ */
+Zotero.Server.Connector.Proxies = {};
+Zotero.Server.Connector.Proxies = function() {};
+Zotero.Server.Endpoints["/connector/proxies"] = Zotero.Server.Connector.Proxies;
+Zotero.Server.Connector.Proxies.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: false,
+
+ /**
+ * Returns a 200 response to say the server is alive
+ */
+ init: Zotero.Promise.coroutine(function* () {
+ let proxies = Zotero.Proxies.proxies.map((p) => Object.assign(p.toJSON(), {hosts: p.hosts}));
+ return [200, "application/json", JSON.stringify(proxies)];
+ })
+};
+
+
+/**
+ * Test connection
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * Nothing (200 OK response)
+ */
+Zotero.Server.Connector.Ping = function() {};
+Zotero.Server.Endpoints["/connector/ping"] = Zotero.Server.Connector.Ping;
+Zotero.Server.Connector.Ping.prototype = {
+ supportedMethods: ["GET", "POST"],
+ supportedDataTypes: ["application/json", "text/plain"],
+ permitBookmarklet: true,
+
+ /**
+ * Sends 200 and HTML status on GET requests
+ * @param data {Object} request information defined in connector.js
+ */
+ init: function (req) {
+ if (req.method == 'GET') {
+ return [200, "text/html", '<!DOCTYPE html><html><head>' +
+ '<title>Zotero Connector Server is Available</title></head>' +
+ '<body>Zotero Connector Server is Available</body></html>'];
+ } else {
+ // Store the active URL so it can be used for site-specific Quick Copy
+ if (req.data.activeURL) {
+ //Zotero.debug("Setting active URL to " + req.data.activeURL);
+ Zotero.QuickCopy.lastActiveURL = req.data.activeURL;
+ }
+
+ let response = {
+ prefs: {
+ automaticSnapshots: Zotero.Prefs.get('automaticSnapshots')
+ }
+ };
+ if (Zotero.QuickCopy.hasSiteSettings()) {
+ response.prefs.reportActiveURL = true;
+ }
+
+ return [200, 'application/json', JSON.stringify(response)];
+ }
+ }
+}
+
+/**
+ * IE messaging hack
+ *
+ * Accepts:
+ * Nothing
+ * Returns:
+ * Static Response
+ */
+Zotero.Server.Connector.IEHack = function() {};
+Zotero.Server.Endpoints["/connector/ieHack"] = Zotero.Server.Connector.IEHack;
+Zotero.Server.Connector.IEHack.prototype = {
+ supportedMethods: ["GET"],
+ permitBookmarklet: true,
+
+ /**
+ * Sends a fixed webpage
+ * @param {String} data POST data or GET query string
+ * @param {Function} sendResponseCallback function to send HTTP response
+ */
+ init: function(postData, sendResponseCallback) {
+ sendResponseCallback(200, "text/html",
+ '<!DOCTYPE html><html><head>'+
+ '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'common_ie.js"></script>'+
+ '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'ie_hack.js"></script>'+
+ '</head><body></body></html>');
+ }
+}
+
+// XXX For compatibility with older connectors; to be removed
+Zotero.Server.Connector.IncompatibleVersion = function() {};
+Zotero.Server.Connector.IncompatibleVersion._errorShown = false
+Zotero.Server.Endpoints["/translate/list"] = Zotero.Server.Connector.IncompatibleVersion;
+Zotero.Server.Endpoints["/translate/detect"] = Zotero.Server.Connector.IncompatibleVersion;
+Zotero.Server.Endpoints["/translate/save"] = Zotero.Server.Connector.IncompatibleVersion;
+Zotero.Server.Endpoints["/translate/select"] = Zotero.Server.Connector.IncompatibleVersion;
+Zotero.Server.Connector.IncompatibleVersion.prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ init: function(postData, sendResponseCallback) {
+ sendResponseCallback(404);
+ if(Zotero.Server.Connector.IncompatibleVersion._errorShown) return;
+
+ Zotero.Utilities.Internal.activate();
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
+ createInstance(Components.interfaces.nsIPromptService);
+ ps.alert(null,
+ Zotero.getString("connector.error.title"),
+ Zotero.getString("integration.error.incompatibleVersion2",
+ ["Standalone "+Zotero.version, "Connector", "2.999.1"]));
+ Zotero.Server.Connector.IncompatibleVersion._errorShown = true;
+ }
+};
+\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js b/chrome/content/zotero/xpcom/connector/server_connectorIntegration.js
@@ -0,0 +1,72 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2017 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ http://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see <http://www.gnu.org/licenses/>.
+
+ ***** END LICENSE BLOCK *****
+*/
+
+/**
+ * Adds integration endpoints related to doc integration via HTTP/connector.
+ *
+ * document/execCommand initiates an integration command and responds with the
+ * next request for the http client (e.g. 'Application.getDocument').
+ * The client should respond to document/respond with the payload and expect
+ * another response with the next request, until it receives 'Document.complete'
+ * at which point the integration transaction is considered complete.
+ */
+Zotero.Server.Endpoints['/connector/document/execCommand'] = function() {};
+Zotero.Server.Endpoints['/connector/document/execCommand'].prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+ init: function(data, sendResponse) {
+ if (Zotero.HTTPIntegrationClient.inProgress) {
+ // This will focus the last integration window if present
+ Zotero.Integration.execCommand('http', data.command, data.docId);
+ sendResponse(503, 'text/plain', 'Integration transaction is already in progress')
+ return;
+ }
+ Zotero.HTTPIntegrationClient.inProgress = true;
+ Zotero.HTTPIntegrationClient.sendResponse = sendResponse;
+ Zotero.Integration.execCommand('http', data.command, data.docId);
+ },
+};
+
+Zotero.Server.Endpoints['/connector/document/respond'] = function() {};
+Zotero.Server.Endpoints['/connector/document/respond'].prototype = {
+ supportedMethods: ["POST"],
+ supportedDataTypes: ["application/json"],
+ permitBookmarklet: true,
+
+ init: function(data, sendResponse) {
+ data = JSON.parse(data);
+ if (data && data.error) {
+ // Apps Script stack is a JSON object
+ if (typeof data.stack != "string") {
+ data.stack = JSON.stringify(data.stack);
+ }
+ Zotero.HTTPIntegrationClient.deferredResponse.reject(data);
+ } else {
+ Zotero.HTTPIntegrationClient.deferredResponse.resolve(data);
+ }
+ Zotero.HTTPIntegrationClient.sendResponse = sendResponse;
+ }
+};
diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js
@@ -52,8 +52,12 @@ const INTEGRATION_TYPE_BIBLIOGRAPHY = 2;
const INTEGRATION_TYPE_TEMP = 3;
const DELAY_CITATIONS_PROMPT_TIMEOUT = 15/*seconds*/;
-const DELAYED_CITATION_STYLING = "\\uldash";
-const DELAYED_CITATION_STYLING_CLEAR = "\\ulclear";
+
+const DELAYED_CITATION_RTF_STYLING = "\\uldash";
+const DELAYED_CITATION_RTF_STYLING_CLEAR = "\\ulclear";
+
+const DELAYED_CITATION_HTML_STYLING = "<div class='delayed-zotero-citation-updates'>"
+const DELAYED_CITATION_HTML_STYLING_END = "</div>"
Zotero.Integration = new function() {
@@ -181,7 +185,11 @@ Zotero.Integration = new function() {
});
this.getApplication = function(agent, command, docId) {
+ if (agent == 'http') {
+ return new Zotero.HTTPIntegrationClient.Application();
+ }
// Try to load the appropriate Zotero component; otherwise display an error
+ var component
try {
var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1";
Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : ""));
@@ -201,116 +209,117 @@ Zotero.Integration = new function() {
/**
* Executes an integration command, first checking to make sure that versions are compatible
*/
- this.execCommand = new function() {
- var inProgress;
+ this.execCommand = async function(agent, command, docId) {
+ var document, session;
- return Zotero.Promise.coroutine(function* execCommand(agent, command, docId) {
- var document, session;
-
- if (inProgress) {
- Zotero.Utilities.Internal.activate();
- if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) {
- Zotero.Integration.currentWindow.focus();
- }
- Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command);
- return;
+ if (Zotero.Integration.currentDoc) {
+ Zotero.Utilities.Internal.activate();
+ if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) {
+ Zotero.Integration.currentWindow.focus();
}
- inProgress = true;
- Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} invoked`)
+ Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command);
+ return;
+ }
+ Zotero.Integration.currentDoc = true;
+ Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} invoked`)
- var startTime = (new Date()).getTime();
+ var startTime = (new Date()).getTime();
- // Try to execute the command; otherwise display an error in alert service or word processor
- // (depending on what is possible)
- try {
- // Word for windows throws RPC_E_CANTCALLOUT_ININPUTSYNCCALL if we invoke an OLE call in the
- // current event loop (which.. who would have guessed would be the case?)
- yield Zotero.Promise.delay();
- var application = Zotero.Integration.getApplication(agent, command, docId);
-
- Zotero.Integration.currentDoc = document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument());
- Zotero.Integration.currentSession = session = yield Zotero.Integration.getSession(application, document, agent);
- // TODO: this is pretty awful
- session.fields = new Zotero.Integration.Fields(session, document);
- session._doc = document;
- // TODO: figure this out
- // Zotero.Notifier.trigger('delete', 'collection', 'document');
- yield (new Zotero.Integration.Interface(application, document, session))[command]();
- document.setDocumentData(session.data.serialize());
+ // Try to execute the command; otherwise display an error in alert service or word processor
+ // (depending on what is possible)
+ try {
+ // Word for windows throws RPC_E_CANTCALLOUT_ININPUTSYNCCALL if we invoke an OLE call in the
+ // current event loop (which.. who would have guessed would be the case?)
+ await Zotero.Promise.delay();
+ var application = Zotero.Integration.getApplication(agent, command, docId);
+
+ var documentPromise = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument());
+ if (!documentPromise.then) {
+ Zotero.debug('Synchronous integration plugin functions are deprecated -- ' +
+ 'update to asynchronous methods');
+ application = Zotero.Integration.LegacyPluginWrapper(application);
+ documentPromise = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument());
}
- catch (e) {
- if(!(e instanceof Zotero.Exception.UserCancelled)) {
- try {
- var displayError = null;
- if(e instanceof Zotero.Exception.Alert) {
- displayError = e.message;
- } else {
- if(e.toString().indexOf("ExceptionAlreadyDisplayed") === -1) {
- displayError = Zotero.getString("integration.error.generic")+"\n\n"+(e.message || e.toString());
- }
- if(e.stack) {
- Zotero.debug(e.stack);
- }
+ Zotero.Integration.currentDoc = document = await documentPromise;
+
+ Zotero.Integration.currentSession = session = await Zotero.Integration.getSession(application, document, agent);
+ // TODO: this is a pretty awful circular dependence
+ session.fields = new Zotero.Integration.Fields(session, document);
+ // TODO: figure this out
+ // Zotero.Notifier.trigger('delete', 'collection', 'document');
+ await (new Zotero.Integration.Interface(application, document, session))[command]();
+ await document.setDocumentData(session.data.serialize());
+ }
+ catch (e) {
+ if(!(e instanceof Zotero.Exception.UserCancelled)) {
+ try {
+ var displayError = null;
+ if(e instanceof Zotero.Exception.Alert) {
+ displayError = e.message;
+ } else {
+ if(e.toString().indexOf("ExceptionAlreadyDisplayed") === -1) {
+ displayError = Zotero.getString("integration.error.generic")+"\n\n"+(e.message || e.toString());
}
+ if(e.stack) {
+ Zotero.debug(e.stack);
+ }
+ }
+
+ if(displayError) {
+ var showErrorInFirefox = !document;
- if(displayError) {
- var showErrorInFirefox = !document;
-
- if(document) {
- try {
- document.activate();
- document.displayAlert(displayError, DIALOG_ICON_STOP, DIALOG_BUTTONS_OK);
- } catch(e) {
- showErrorInFirefox = true;
- }
- }
-
- if(showErrorInFirefox) {
- Zotero.Utilities.Internal.activate();
- Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
- .getService(Components.interfaces.nsIPromptService)
- .alert(null, Zotero.getString("integration.error.title"), displayError);
+ if(document) {
+ try {
+ await document.activate();
+ await document.displayAlert(displayError, DIALOG_ICON_STOP, DIALOG_BUTTONS_OK);
+ } catch(e) {
+ showErrorInFirefox = true;
}
}
- } finally {
- Zotero.logError(e);
- }
- } else {
- // If user cancels we should still write the currently assigned session ID
- document.setDocumentData(session.data.serialize());
- }
- }
- finally {
- var diff = ((new Date()).getTime() - startTime)/1000;
- Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} complete in ${diff}s`)
- if (document) {
- try {
- document.cleanup();
- document.activate();
- // Call complete function if one exists
- if (document.wrappedJSObject && document.wrappedJSObject.complete) {
- document.wrappedJSObject.complete();
- } else if (document.complete) {
- document.complete();
+ if(showErrorInFirefox) {
+ Zotero.Utilities.Internal.activate();
+ Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService)
+ .alert(null, Zotero.getString("integration.error.title"), displayError);
}
- } catch(e) {
- Zotero.logError(e);
}
+ } finally {
+ Zotero.logError(e);
}
-
- if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) {
- var oldWindow = Zotero.Integration.currentWindow;
- Zotero.Promise.delay(100).then(function() {
- oldWindow.close();
- });
+ } else {
+ // If user cancels we should still write the currently assigned session ID
+ await document.setDocumentData(session.data.serialize());
+ }
+ }
+ finally {
+ var diff = ((new Date()).getTime() - startTime)/1000;
+ Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} complete in ${diff}s`)
+ if (document) {
+ try {
+ await document.cleanup();
+ await document.activate();
+
+ // Call complete function if one exists
+ if (document.wrappedJSObject && document.wrappedJSObject.complete) {
+ document.wrappedJSObject.complete();
+ } else if (document.complete) {
+ await document.complete();
+ }
+ } catch(e) {
+ Zotero.logError(e);
}
-
- inProgress =
- Zotero.Integration.currentDoc =
- Zotero.Integration.currentWindow = false;
}
- });
+
+ if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) {
+ var oldWindow = Zotero.Integration.currentWindow;
+ Zotero.Promise.delay(100).then(function() {
+ oldWindow.close();
+ });
+ }
+
+ Zotero.Integration.currentDoc = Zotero.Integration.currentWindow = false;
+ }
};
/**
@@ -320,8 +329,8 @@ Zotero.Integration = new function() {
* @param {String} [io] Data to pass to the window
* @return {Promise} Promise resolved when the window is closed
*/
- this.displayDialog = function displayDialog(url, options, io) {
- Zotero.Integration.currentDoc.cleanup();
+ this.displayDialog = async function displayDialog(url, options, io) {
+ await Zotero.Integration.currentDoc.cleanup();
var allOptions = 'chrome,centerscreen';
// without this, Firefox gets raised with our windows under Compiz
@@ -350,7 +359,7 @@ Zotero.Integration = new function() {
}
window.addEventListener("unload", listener, false);
- return deferred.promise;
+ await deferred.promise;
};
/**
@@ -358,8 +367,8 @@ Zotero.Integration = new function() {
* Either loads a cached session if doc communicated since restart or creates a new one
* @return {Zotero.Integration.Session} Promise
*/
- this.getSession = Zotero.Promise.coroutine(function *(app, doc, agent) {
- var dataString = doc.getDocumentData(),
+ this.getSession = async function (app, doc, agent) {
+ var dataString = await doc.getDocumentData(),
data, session;
try {
@@ -372,12 +381,12 @@ Zotero.Integration = new function() {
if (data.dataVersion < DATA_VERSION) {
if (data.dataVersion == 1
&& data.prefs.fieldType == "Field"
- && this._app.primaryFieldType == "ReferenceMark") {
+ && app.primaryFieldType == "ReferenceMark") {
// Converted OOo docs use ReferenceMarks, not fields
data.prefs.fieldType = "ReferenceMark";
}
- var warning = doc.displayAlert(Zotero.getString("integration.upgradeWarning", [Zotero.clientName, '5.0']),
+ var warning = await doc.displayAlert(Zotero.getString("integration.upgradeWarning", [Zotero.clientName, '5.0']),
DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL);
if (!warning) {
throw new Zotero.Exception.UserCancelled("document upgrade");
@@ -401,26 +410,25 @@ Zotero.Integration = new function() {
session.reload = true;
}
try {
- yield session.setData(data);
+ await session.setData(data);
} catch(e) {
// make sure style is defined
if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") {
if (data.style.styleID) {
- session.reload = true;
let trustedSource = /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID);
let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID);
if (trustedSource ||
- doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) {
-
+ await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) {
+
let installed = false;
try {
- yield Zotero.Styles.install(
+ await Zotero.Styles.install(
{url: data.style.styleID}, data.style.styleID, true
);
installed = true;
}
catch (e) {
- doc.displayAlert(
+ await doc.displayAlert(
Zotero.getString(
'integration.error.styleNotFound', data.style.styleID
),
@@ -429,19 +437,20 @@ Zotero.Integration = new function() {
);
}
if (installed) {
- yield session.setData(data, true);
+ await session.setData(data, true);
}
return session;
}
}
- yield session.setDocPrefs();
+ await session.setDocPrefs();
} else {
throw e;
}
}
session.agent = agent;
+ session._doc = doc;
return session;
- });
+ };
}
@@ -478,7 +487,7 @@ Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(fu
yield this._session.init(false, false);
let [idx, field, citation] = yield this._session.fields.addEditCitation(null);
- yield this._session.addCitation(idx, field.getNoteIndex(), citation);
+ yield this._session.addCitation(idx, yield field.getNoteIndex(), citation);
if (this._session.data.prefs.delayCitationUpdates) {
return this._session.writeDelayedCitation(idx, field, citation);
@@ -493,7 +502,7 @@ Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(fu
*/
Zotero.Integration.Interface.prototype.editCitation = Zotero.Promise.coroutine(function* () {
yield this._session.init(true, false);
- var docField = this._doc.cursorInField(this._session.data.prefs['fieldType']);
+ var docField = yield this._doc.cursorInField(this._session.data.prefs['fieldType']);
if(!docField) {
throw new Zotero.Exception.Alert("integration.error.notInCitation", [],
"integration.error.title");
@@ -505,18 +514,18 @@ Zotero.Integration.Interface.prototype.editCitation = Zotero.Promise.coroutine(f
* Edits the citation at the cursor position if one exists, or else adds a new one.
* @return {Promise}
*/
-Zotero.Integration.Interface.prototype.addEditCitation = Zotero.Promise.coroutine(function* (docField) {
- yield this._session.init(false, false);
- docField = docField || this._doc.cursorInField(this._session.data.prefs['fieldType']);
+Zotero.Integration.Interface.prototype.addEditCitation = async function (docField) {
+ await this._session.init(false, false);
+ docField = docField || await this._doc.cursorInField(this._session.data.prefs['fieldType']);
- let [idx, field, citation] = yield this._session.fields.addEditCitation(docField);
- yield this._session.addCitation(idx, field.getNoteIndex(), citation);
+ let [idx, field, citation] = await this._session.fields.addEditCitation(docField);
+ await this._session.addCitation(idx, await field.getNoteIndex(), citation);
if (this._session.data.prefs.delayCitationUpdates) {
return this._session.writeDelayedCitation(idx, field, citation);
} else {
return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false);
}
-});
+};
/**
* Adds a bibliography to the current document.
@@ -532,8 +541,8 @@ Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutin
}
let field = new Zotero.Integration.BibliographyField(yield this._session.fields.addField());
- field.clearCode();
var citationsMode = FORCE_CITATIONS_FALSE;
+ yield field.clearCode();
if(this._session.data.prefs.delayCitationUpdates) {
// Refreshes citeproc state before proceeding
this._session.reload = true;
@@ -554,7 +563,7 @@ Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.corouti
var bibliographyField;
for (let i = fields.length-1; i >= 0; i--) {
- let field = Zotero.Integration.Field.loadExisting(fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(fields[i]);
if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) {
bibliographyField = field;
break;
@@ -565,7 +574,7 @@ Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.corouti
throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography",
[], "integration.error.title");
}
- let bibliography = new Zotero.Integration.Bibliography(bibliographyField, bibliographyField.unserialize());
+ let bibliography = new Zotero.Integration.Bibliography(bibliographyField, yield bibliographyField.unserialize());
var citationsMode = FORCE_CITATIONS_FALSE;
if(this._session.data.prefs.delayCitationUpdates) {
// Refreshes citeproc state before proceeding
@@ -591,7 +600,7 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro
var bibliographyField;
for (let i = fields.length-1; i >= 0; i--) {
- let field = Zotero.Integration.Field.loadExisting(fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(fields[i]);
if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) {
bibliographyField = field;
break;
@@ -601,10 +610,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro
var newBibliography = !bibliographyField;
if (!bibliographyField) {
bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField());
- bibliographyField.clearCode();
+ yield bibliographyField.clearCode();
}
- let bibliography = new Zotero.Integration.Bibliography(bibliographyField, bibliographyField.unserialize());
+ let bibliography = new Zotero.Integration.Bibliography(bibliographyField, yield bibliographyField.unserialize());
var citationsMode = FORCE_CITATIONS_FALSE;
if(this._session.data.prefs.delayCitationUpdates) {
// Refreshes citeproc state before proceeding
@@ -636,11 +645,11 @@ Zotero.Integration.Interface.prototype.removeCodes = Zotero.Promise.coroutine(fu
var me = this;
yield this._session.init(true, false)
let fields = yield this._session.fields.get()
- var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"),
+ var result = yield me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"),
DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL);
if (result) {
for(var i=fields.length-1; i>=0; i--) {
- fields[i].removeCode();
+ yield fields[i].removeCode();
}
}
})
@@ -680,10 +689,10 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
var fieldsToConvert = new Array();
var fieldNoteTypes = new Array();
for (var i=0, n=fields.length; i<n; i++) {
- let field = Zotero.Integration.Field.loadExisting(fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(fields[i]);
if (convertItems && field.type === INTEGRATION_TYPE_ITEM) {
- var citation = field.unserialize();
+ var citation = yield field.unserialize();
if (!citation.properties.dontUpdate) {
fieldsToConvert.push(fields[i]);
fieldNoteTypes.push(this._session.data.prefs.noteType);
@@ -697,7 +706,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu
if(fieldsToConvert.length) {
// Pass to conversion function
- this._doc.convert(new Zotero.Integration.JSEnumerator(fieldsToConvert),
+ yield this._doc.convert(fieldsToConvert,
this._session.data.prefs.fieldType, fieldNoteTypes,
fieldNoteTypes.length);
}
@@ -747,16 +756,16 @@ Zotero.Integration.Fields = function(session, doc) {
* Checks that it is appropriate to add fields to the current document at the current
* positon, then adds one.
*/
-Zotero.Integration.Fields.prototype.addField = function(note) {
+Zotero.Integration.Fields.prototype.addField = async function(note) {
// Get citation types if necessary
- if (!this._doc.canInsertField(this._session.data.prefs['fieldType'])) {
+ if (!await this._doc.canInsertField(this._session.data.prefs['fieldType'])) {
return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.cannotInsertHere",
[], "integration.error.title"));
}
- var field = this._doc.cursorInField(this._session.data.prefs['fieldType']);
+ var field = await this._doc.cursorInField(this._session.data.prefs['fieldType']);
if (field) {
- if (!this._session.displayAlert(Zotero.getString("integration.replace"),
+ if (!await this._session.displayAlert(Zotero.getString("integration.replace"),
DIALOG_ICON_STOP,
DIALOG_BUTTONS_OK_CANCEL)) {
return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("inserting citation"));
@@ -764,7 +773,7 @@ Zotero.Integration.Fields.prototype.addField = function(note) {
}
if (!field) {
- field = this._doc.insertField(this._session.data.prefs['fieldType'],
+ field = await this._doc.insertField(this._session.data.prefs['fieldType'],
(note ? this._session.data.prefs["noteType"] : 0));
// Older doc plugins do not initialize the field code to anything meaningful
// so we ensure it here manually
@@ -785,10 +794,10 @@ Zotero.Integration.Fields.prototype.addField = function(note) {
*/
Zotero.Integration.Fields.prototype.get = new function() {
var deferred;
- return function() {
+ return async function() {
// If we already have fields, just return them
- if(this._fields) {
- return Zotero.Promise.resolve(this._fields);
+ if(this._fields != undefined) {
+ return this._fields;
}
if (deferred) {
@@ -798,59 +807,20 @@ Zotero.Integration.Fields.prototype.get = new function() {
var promise = deferred.promise;
// Otherwise, start getting fields
- var getFieldsTime = (new Date()).getTime(),
- me = this;
- this._doc.getFieldsAsync(this._session.data.prefs['fieldType'],
- {"observe":function(subject, topic, data) {
- if(topic === "fields-available") {
- if(me.progressCallback) {
- try {
- me.progressCallback(75);
- } catch(e) {
- Zotero.logError(e);
- };
- }
-
- try {
- // Add fields to fields array
- var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator);
- var fields = me._fields = [];
- while(fieldsEnumerator.hasMoreElements()) {
- let field = fieldsEnumerator.getNext();
- try {
- fields.push(field.QueryInterface(Components.interfaces.zoteroIntegrationField));
- } catch (e) {
- fields.push(field);
- }
- }
-
- if(Zotero.Debug.enabled) {
- var endTime = (new Date()).getTime();
- Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+
- (endTime-getFieldsTime)/1000+"; "+
- 1000/((endTime-getFieldsTime)/fields.length)+" fields/second");
- }
- } catch(e) {
- deferred.reject(e);
- deferred = null;
- return;
- }
-
- deferred.resolve(fields);
- deferred = null;
- } else if(topic === "fields-progress") {
- if(me.progressCallback) {
- try {
- me.progressCallback((data ? parseInt(data, 10)*(3/4) : null));
- } catch(e) {
- Zotero.logError(e);
- };
- }
- } else if(topic === "fields-error") {
- deferred.reject(data);
- deferred = null;
- }
- }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])});
+ var getFieldsTime = (new Date()).getTime();
+ try {
+ var fields = this._fields = Array.from(await this._doc.getFields(this._session.data.prefs['fieldType']));
+
+ var endTime = (new Date()).getTime();
+ Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+
+ (endTime-getFieldsTime)/1000+"; "+
+ 1000/((endTime-getFieldsTime)/fields.length)+" fields/second");
+ deferred.resolve(fields);
+ } catch(e) {
+ deferred.reject(e);
+ }
+
+ deferred = null;
return promise;
}
}
@@ -896,15 +866,15 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu
}
for (var i = 0; i < this._fields.length; i++) {
- let field = Zotero.Integration.Field.loadExisting(this._fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(this._fields[i]);
if (field.type === INTEGRATION_TYPE_ITEM) {
- var noteIndex = field.getNoteIndex(),
- data = field.unserialize(),
- citation = new Zotero.Integration.Citation(field, noteIndex, data);
+ var noteIndex = yield field.getNoteIndex(),
+ data = yield field.unserialize(),
+ citation = new Zotero.Integration.Citation(field, data, noteIndex);
yield this._session.addCitation(i, noteIndex, citation);
} else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) {
- if (this.ignoreEmptyBibliography && field.getText().trim() === "") {
+ if (this.ignoreEmptyBibliography && (yield field.getText()).trim() === "") {
this._removeCodeFields[i] = true;
} else {
this._bibliographyFields.push(field);
@@ -912,7 +882,7 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu
}
}
if (this._bibliographyFields.length) {
- var data = this._bibliographyFields[0].unserialize()
+ var data = yield this._bibliographyFields[0].unserialize()
this._session.bibliography = new Zotero.Integration.Bibliography(this._bibliographyFields[0], data);
yield this._session.bibliography.loadItemData();
} else {
@@ -943,14 +913,14 @@ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(fu
Zotero.debug(`Integration: updateDocument complete in ${diff}s`)
// If the update takes longer than 5s suggest delaying citation updates
if (diff > DELAY_CITATIONS_PROMPT_TIMEOUT && !this._session.data.prefs.dontAskDelayCitationUpdates && !this._session.data.prefs.delayCitationUpdates) {
- this._doc.activate();
+ yield this._doc.activate();
var interfaceType = 'tab';
if (['MacWord2008', 'OpenOffice'].includes(this._session.agent)) {
interfaceType = 'toolbar';
}
- var result = this._session.displayAlert(
+ var result = yield this._session.displayAlert(
Zotero.getString('integration.delayCitationUpdates.alert.text1')
+ "\n\n"
+ Zotero.getString(`integration.delayCitationUpdates.alert.text2.${interfaceType}`)
@@ -1004,7 +974,7 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
if (!citation.properties.dontUpdate) {
var formattedCitation = citation.properties.custom
? citation.properties.custom : citation.text;
- var plainCitation = citation.properties.plainCitation && citationField.getText();
+ var plainCitation = citation.properties.plainCitation && await citationField.getText();
var plaintextChanged = citation.properties.plainCitation
&& plainCitation !== citation.properties.plainCitation;
@@ -1014,8 +984,8 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
+ "Original: " + citation.properties.plainCitation + "\n"
+ "Current: " + plainCitation
);
- citationField.select();
- var result = this._session.displayAlert(
+ await citationField.select();
+ var result = await this._session.displayAlert(
Zotero.getString("integration.citationChanged")+"\n\n"
+ Zotero.getString("integration.citationChanged.description")+"\n\n"
+ Zotero.getString("integration.citationChanged.original", citation.properties.plainCitation)+"\n"
@@ -1037,20 +1007,22 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
// Word will preserve previous text styling, so we need to force remove it
// for citations that were inserted with delay styling
- if (citation.properties.formattedCitation && citation.properties.formattedCitation.includes(DELAYED_CITATION_STYLING)) {
- isRich = citationField.setText(`${DELAYED_CITATION_STYLING_CLEAR}{${formattedCitation}}`);
+ var wasDelayed = citation.properties.formattedCitation
+ && citation.properties.formattedCitation.includes(DELAYED_CITATION_RTF_STYLING);
+ if (this._session.outputFormat == 'rtf' && wasDelayed) {
+ isRich = await citationField.setText(`${DELAYED_CITATION_RTF_STYLING_CLEAR}{${formattedCitation}}`);
} else {
- isRich = citationField.setText(formattedCitation);
+ isRich = await citationField.setText(formattedCitation);
}
citation.properties.formattedCitation = formattedCitation;
- citation.properties.plainCitation = citationField.getText();
+ citation.properties.plainCitation = await citationField.getText();
}
}
var serializedCitation = citation.serialize();
if (serializedCitation != citation.properties.field) {
- citationField.setCode(serializedCitation);
+ await citationField.setCode(serializedCitation);
}
nUpdated++;
}
@@ -1064,7 +1036,7 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
if (forceBibliography || this._session.bibliographyDataHasChanged) {
let code = this._session.bibliography.serialize();
for (let field of this._bibliographyFields) {
- field.setCode(code);
+ await field.setCode(code);
}
}
@@ -1073,14 +1045,18 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
var bibliographyText = "";
if (bib) {
- bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend;
+ if (this._session.outputFormat == 'rtf') {
+ bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend;
+ } else {
+ bibliographyText = bib[0].bibstart+bib[1].join("")+bib[0].bibend;
+ }
// if bibliography style not set, set it
if(!this._session.data.style.bibliographyStyleHasBeenSet) {
var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib);
// set bibliography style
- this._doc.setBibliographyStyle(bibStyle.firstLineIndent, bibStyle.indent,
+ await this._doc.setBibliographyStyle(bibStyle.firstLineIndent, bibStyle.indent,
bibStyle.lineSpacing, bibStyle.entrySpacing, bibStyle.tabStops, bibStyle.tabStops.length);
// set bibliographyStyleHasBeenSet parameter to prevent further changes
@@ -1101,9 +1077,9 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
await Zotero.Promise.delay();
if (bibliographyText) {
- field.setText(bibliographyText);
+ await field.setText(bibliographyText);
} else {
- field.setText("{Bibliography}");
+ await field.setText("{Bibliography}");
}
nUpdated += 5;
}
@@ -1112,7 +1088,7 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
// Do these operations in reverse in case plug-ins care about order
var removeCodeFields = Object.keys(this._removeCodeFields).sort();
for (var i=(removeCodeFields.length-1); i>=0; i--) {
- this._fields[removeCodeFields[i]].removeCode();
+ await this._fields[removeCodeFields[i]].removeCode();
}
var deleteFields = Object.keys(this._deleteFields).sort();
@@ -1124,30 +1100,30 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati
/**
* Brings up the addCitationDialog, prepopulated if a citation is provided
*/
-Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) {
+Zotero.Integration.Fields.prototype.addEditCitation = async function (field) {
var newField;
var citation;
if (field) {
- field = Zotero.Integration.Field.loadExisting(field);
+ field = await Zotero.Integration.Field.loadExisting(field);
if (field.type != INTEGRATION_TYPE_ITEM) {
throw new Zotero.Exception.Alert("integration.error.notInCitation");
}
- citation = new Zotero.Integration.Citation(field, field.getNoteIndex(), field.unserialize());
+ citation = new Zotero.Integration.Citation(field, await field.unserialize(), await field.getNoteIndex());
} else {
newField = true;
- field = new Zotero.Integration.CitationField(yield this.addField(true));
+ field = new Zotero.Integration.CitationField(await this.addField(true));
citation = new Zotero.Integration.Citation(field);
}
- yield citation.prepareForEditing();
+ await citation.prepareForEditing();
// -------------------
// Preparing stuff to pass into CitationEditInterface
- var fieldIndexPromise = this.get().then(function(fields) {
+ var fieldIndexPromise = this.get().then(async function(fields) {
for (var i=0, n=fields.length; i<n; i++) {
- if (fields[i].equals(field._field)) {
+ if (await fields[i].equals(field._field)) {
// This is needed, because LibreOffice integration plugin caches the field code instead of asking
// the document every time when calling #getCode().
field = new Zotero.Integration.CitationField(fields[i]);
@@ -1160,9 +1136,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f
if (this._session.data.prefs.delayCitationUpdates) {
citationsByItemIDPromise = Zotero.Promise.resolve(this._session.citationsByItemID);
} else {
- citationsByItemIDPromise = fieldIndexPromise.then(function() {
- return this.updateSession(FORCE_CITATIONS_FALSE);
- }.bind(this)).then(function() {
+ citationsByItemIDPromise = this.updateSession(FORCE_CITATIONS_FALSE).then(function() {
return this._session.citationsByItemID;
}.bind(this));
}
@@ -1211,24 +1185,24 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f
// -------------------
// io.promise resolves when the citation dialog is closed
- this.progressCallback = yield io.promise;
+ this.progressCallback = await io.promise;
if (!io.citation.citationItems.length) {
// Try to delete new field on cancel
if (newField) {
try {
- yield field.delete();
+ await field.delete();
} catch(e) {}
}
throw new Zotero.Exception.UserCancelled("inserting citation");
}
- var fieldIndex = yield fieldIndexPromise;
+ var fieldIndex = await fieldIndexPromise;
this._session.updateIndices[fieldIndex] = true;
// Make sure session is updated
- yield citationsByItemIDPromise;
+ await citationsByItemIDPromise;
return [fieldIndex, field, io.citation];
-});
+};
/**
* Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js
@@ -1279,9 +1253,9 @@ Zotero.Integration.CitationEditInterface.prototype = {
* Get a list of items used in the current document
* @return {Promise} A promise resolved by the items
*/
- getItems: Zotero.Promise.coroutine(function* () {
- var fieldIndex = yield this._fieldIndexPromise;
- var citationsByItemID = yield this._citationsByItemIDPromise;
+ getItems: async function () {
+ var fieldIndex = await this._fieldIndexPromise;
+ var citationsByItemID = await this._citationsByItemIDPromise;
var ids = Object.keys(citationsByItemID).filter(itemID => {
return citationsByItemID[itemID]
&& citationsByItemID[itemID].length
@@ -1305,7 +1279,7 @@ Zotero.Integration.CitationEditInterface.prototype = {
});
return Zotero.Cite.getItem(ids);
- }),
+ },
}
/**
@@ -1319,6 +1293,8 @@ Zotero.Integration.Session = function(doc, app) {
this.resetRequest(doc);
this.primaryFieldType = app.primaryFieldType;
this.secondaryFieldType = app.secondaryFieldType;
+ this.outputFormat = app.outputFormat || 'rtf';
+ this._app = app;
this.sessionID = Zotero.randomString();
Zotero.Integration.sessions[this.sessionID] = this;
@@ -1370,8 +1346,8 @@ Zotero.Integration.Session.prototype.init = Zotero.Promise.coroutine(function *(
if (require && data.prefs.fieldType) {
// check to see if fields already exist
for (let fieldType of [this.primaryFieldType, this.secondaryFieldType]) {
- var fields = this._doc.getFields(fieldType);
- if (fields.hasMoreElements()) {
+ var fields = yield this._doc.getFields(fieldType);
+ if (fields.length) {
data.prefs.fieldType = fieldType;
haveFields = true;
break;
@@ -1396,11 +1372,11 @@ Zotero.Integration.Session.prototype.init = Zotero.Promise.coroutine(function *(
return true;
});
-Zotero.Integration.Session.prototype.displayAlert = function() {
+Zotero.Integration.Session.prototype.displayAlert = async function() {
if (this.timer) {
this.timer.pause();
}
- var result = this._doc.displayAlert.apply(this._doc, arguments);
+ var result = await this._doc.displayAlert.apply(this._doc, arguments);
if (this.timer) {
this.timer.resume();
}
@@ -1414,21 +1390,21 @@ Zotero.Integration.Session.prototype.displayAlert = function() {
* regardless of whether it has changed. This is desirable if the
* automaticJournalAbbreviations or locale has changed.
*/
-Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function *(data, resetStyle) {
+Zotero.Integration.Session.prototype.setData = async function (data, resetStyle) {
var oldStyle = (this.data && this.data.style ? this.data.style : false);
this.data = data;
this.data.sessionID = this.sessionID;
if (data.style.styleID && (!oldStyle || oldStyle.styleID != data.style.styleID || resetStyle)) {
- // We're changing the citeproc instance, so we'll have to reinsert all citations into the registry
- this.reload = true;
- this.styleID = data.style.styleID;
try {
- yield Zotero.Styles.init();
+ await Zotero.Styles.init();
var getStyle = Zotero.Styles.get(data.style.styleID);
data.style.hasBibliography = getStyle.hasBibliography;
this.style = getStyle.getCiteProc(data.style.locale, data.prefs.automaticJournalAbbreviations);
- this.style.setOutputFormat("rtf");
+ this.style.setOutputFormat(this.outputFormat);
this.styleClass = getStyle.class;
+ // We're changing the citeproc instance, so we'll have to reinsert all citations into the registry
+ this.reload = true;
+ this.styleID = data.style.styleID;
} catch (e) {
Zotero.logError(e);
throw new Zotero.Exception.Alert("integration.error.invalidStyle");
@@ -1439,7 +1415,7 @@ Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function
data.style = oldStyle;
}
return false;
-});
+};
/**
* Displays a dialog to set document preferences
@@ -1455,6 +1431,7 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
if (this.data) {
io.style = this.data.style.styleID;
io.locale = this.data.style.locale;
+ io.supportedNotes = this._app.supportedNotes;
io.useEndnotes = this.data.prefs.noteType == 0 ? 0 : this.data.prefs.noteType-1;
io.fieldType = this.data.prefs.fieldType;
io.delayCitationUpdates = this.data.prefs.delayCitationUpdates;
@@ -1475,9 +1452,11 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
// set data
var oldData = this.data;
var data = new Zotero.Integration.DocumentData();
+ data.dataVersion = oldData.dataVersion;
data.sessionID = oldData.sessionID;
data.style.styleID = io.style;
data.style.locale = io.locale;
+ data.style.bibliographyStyleHasBeenSet = false;
data.prefs = oldData ? Object.assign({}, oldData.prefs) : {};
data.prefs.fieldType = io.fieldType;
data.prefs.automaticJournalAbbreviations = io.automaticJournalAbbreviations;
@@ -1671,41 +1650,45 @@ Zotero.Integration.Session.prototype.restoreProcessorState = function() {
citations.push(this.citationsByIndex[i]);
}
}
- this.style.rebuildProcessorState(citations, 'rtf', uncited);
+ this.style.rebuildProcessorState(citations, this.outputFormat, uncited);
}
Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.coroutine(function* (idx, field, citation) {
try {
- var text = citation.properties.custom || this.style.previewCitationCluster(citation, [], [], "rtf");
+ var text = citation.properties.custom || this.style.previewCitationCluster(citation, [], [], this.outputFormat);
} catch(e) {
throw e;
}
- text = `${DELAYED_CITATION_STYLING}{${text}}`;
+ if (this.outputFormat == 'rtf') {
+ text = `${DELAYED_CITATION_RTF_STYLING}{${text}}`;
+ } else {
+ text = `${DELAYED_CITATION_HTML_STYLING}${text}${DELAYED_CITATION_HTML_STYLING_END}`;
+ }
// Make sure we'll prompt for manually edited citations
var isRich = false;
if(!citation.properties.dontUpdate) {
- isRich = field.setText(text);
+ isRich = yield field.setText(text);
citation.properties.formattedCitation = text;
- citation.properties.plainCitation = field.getText();
+ citation.properties.plainCitation = yield field._field.getText();
}
- field.setCode(citation.serialize());
+ yield field.setCode(citation.serialize());
// Update bibliography with a static string
var fields = yield this.fields.get();
var bibliographyField;
for (let i = fields.length-1; i >= 0; i--) {
- let field = Zotero.Integration.Field.loadExisting(fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(fields[i]);
if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) {
var interfaceType = 'tab';
if (['MacWord2008', 'OpenOffice'].includes(this.agent)) {
interfaceType = 'toolbar';
}
- field.setText(Zotero.getString(`integration.delayCitationUpdates.bibliography.${interfaceType}`), false)
+ yield field.setText(Zotero.getString(`integration.delayCitationUpdates.bibliography.${interfaceType}`), false)
break;
}
}
@@ -2061,14 +2044,14 @@ Zotero.Integration.Field = class {
this.type = INTEGRATION_TYPE_TEMP;
}
- setCode(code) {
+ async setCode(code) {
// Boo. Inconsistent order.
if (this.type == INTEGRATION_TYPE_ITEM) {
- this._field.setCode(`ITEM CSL_CITATION ${code}`);
+ await this._field.setCode(`ITEM CSL_CITATION ${code}`);
} else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) {
- this._field.setCode(`BIBL ${code} CSL_BIBLIOGRAPHY`);
+ await this._field.setCode(`BIBL ${code} CSL_BIBLIOGRAPHY`);
} else {
- this._field.setCode(`TEMP`);
+ await this._field.setCode(`TEMP`);
}
this._code = code;
}
@@ -2084,11 +2067,11 @@ Zotero.Integration.Field = class {
return this._code.substring(start, this._code.lastIndexOf('}')+1);
}
- clearCode() {
- this.setCode('{}');
+ async clearCode() {
+ return await this.setCode('{}');
}
- setText(text) {
+ async setText(text) {
var isRich = false;
// If RTF wrap with RTF tags
if (text.includes("\\")) {
@@ -2097,7 +2080,7 @@ Zotero.Integration.Field = class {
}
isRich = true;
}
- this._field.setText(text, isRich);
+ await this._field.setText(text, isRich);
return isRich;
}
};
@@ -2111,11 +2094,11 @@ Zotero.Integration.Field.INTERFACE = ['delete', 'removeCode', 'select', 'setText
* @param idx
* @returns {Zotero.Integration.Field|Zotero.Integration.CitationField|Zotero.Integration.BibliographyField}
*/
-Zotero.Integration.Field.loadExisting = function(docField) {
+Zotero.Integration.Field.loadExisting = async function(docField) {
var field;
// Already loaded
if (docField instanceof Zotero.Integration.Field) return docField;
- var rawCode = docField.getCode();
+ let rawCode = await docField.getCode();
// ITEM/CITATION CSL_ITEM {json: 'data'}
for (let type of ["ITEM", "CITATION"]) {
@@ -2150,7 +2133,7 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field {
*
* @returns {{citationItems: Object[], properties: Object}}
*/
- unserialize() {
+ async unserialize() {
function unserialize(code) {
try {
return JSON.parse(code);
@@ -2260,17 +2243,17 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field {
}
}
- clearCode() {
- this.setCode(JSON.stringify({citationItems: [], properties: {}}));
+ async clearCode() {
+ await this.setCode(JSON.stringify({citationItems: [], properties: {}}));
}
- resolveCorrupt(code) {
+ async resolveCorrupt(code) {
Zotero.debug(`Integration: handling corrupt citation field ${code}`);
var msg = Zotero.getString("integration.corruptField")+'\n\n'+
Zotero.getString('integration.corruptField.description');
- this.select();
+ await this.select();
Zotero.Integration.currentDoc.activate();
- var result = Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL);
+ var result = await Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL);
if (result == 0) { // Cancel
return new Zotero.Exception.UserCancelled("corrupt citation resolution");
} else if (result == 1) { // No
@@ -2280,7 +2263,7 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field {
oldWindow = Zotero.Integration.currentWindow,
oldProgressCallback = this.progressCallback;
// Clear current code and subsequent addEditCitation dialog will be the reselection
- this.clearCode();
+ await this.clearCode();
return this.unserialize();
}
}
@@ -2293,31 +2276,29 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field {
this.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
};
- unserialize() {
- var code = this.getCode();
+ async unserialize() {
try {
- return JSON.parse(code);
+ return JSON.parse(this.getCode());
} catch(e) {
return this.resolveCorrupt(code);
}
}
-
- resolveCorrupt(code) {
+ async resolveCorrupt(code) {
Zotero.debug(`Integration: handling corrupt bibliography field ${code}`);
var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+
Zotero.getString('integration.corruptBibliography.description');
- var result = Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL);
+ var result = await Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL);
if (result == 0) {
throw new Zotero.Exception.UserCancelled("corrupt bibliography resolution");
} else {
- this.clearCode();
+ await this.clearCode();
return unserialize();
}
}
};
Zotero.Integration.Citation = class {
- constructor(citationField, noteIndex, data) {
+ constructor(citationField, data, noteIndex) {
if (!data) {
data = {citationItems: [], properties: {}};
}
@@ -2436,70 +2417,65 @@ Zotero.Integration.Citation = class {
}).apply(this, arguments);
}
- handleMissingItem() {
- return Zotero.Promise.coroutine(function* (idx) {
- // Ask user what to do with this item
- if (this.citationItems.length == 1) {
- var msg = Zotero.getString("integration.missingItem.single");
- } else {
- var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString());
- }
- msg += '\n\n'+Zotero.getString('integration.missingItem.description');
- this._field.select();
- Zotero.Integration.currentDoc.activate();
- var result = Zotero.Integration.currentSession.displayAlert(msg,
- DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL);
- if (result == 0) { // Cancel
- throw new Zotero.Exception.UserCancelled("document update");
- } else if(result == 1) { // No
- return false;
- }
-
- // Yes - prompt to reselect
- var io = new function() { this.wrappedJSObject = this; };
-
- io.addBorder = Zotero.isWin;
- io.singleSelection = true;
+ async handleMissingItem() {
+ // Ask user what to do with this item
+ if (this.citationItems.length == 1) {
+ var msg = Zotero.getString("integration.missingItem.single");
+ } else {
+ var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString());
+ }
+ msg += '\n\n'+Zotero.getString('integration.missingItem.description');
+ await this._field.select();
+ await Zotero.Integration.currentDoc.activate();
+ var result = await Zotero.Integration.currentSession.displayAlert(msg,
+ DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL);
+ if (result == 0) { // Cancel
+ throw new Zotero.Exception.UserCancelled("document update");
+ } else if(result == 1) { // No
+ return false;
+ }
+
+ // Yes - prompt to reselect
+ var io = new function() { this.wrappedJSObject = this; };
+
+ io.addBorder = Zotero.isWin;
+ io.singleSelection = true;
+
+ await Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io);
- yield Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io);
-
- if (io.dataOut && io.dataOut.length) {
- return Zotero.Items.get(io.dataOut[0]);
- }
- }).apply(this, arguments);
- }
-
- prepareForEditing() {
- return Zotero.Promise.coroutine(function *(){
- // Check for modified field text or dontUpdate flag
- var fieldText = this._field.getText();
- if (this.properties.dontUpdate
- || (this.properties.plainCitation
- && fieldText !== this.properties.plainCitation)) {
- Zotero.Integration.currentDoc.activate();
- Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n"
- + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n"
- + "Original: " + this.properties.plainCitation + "\n"
- + "Current: " + fieldText
- );
- if (!Zotero.Integration.currentSession.displayAlert(
+ if (io.dataOut && io.dataOut.length) {
+ return Zotero.Items.get(io.dataOut[0]);
+ }
+ }
+
+ async prepareForEditing() {
+ // Check for modified field text or dontUpdate flag
+ if (this.properties.dontUpdate
+ || (this.properties.plainCitation
+ && await this._field.getText() !== this.properties.plainCitation)) {
+ await Zotero.Integration.currentDoc.activate();
+ var fieldText = await this._field.getText();
+ Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n"
+ + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n"
+ + "Original: " + this.properties.plainCitation + "\n"
+ + "Current: " + fieldText
+ );
+ if (!await Zotero.Integration.currentDoc.displayAlert(
Zotero.getString("integration.citationChanged.edit")+"\n\n"
- + Zotero.getString("integration.citationChanged.original", this.properties.plainCitation)+"\n"
- + Zotero.getString("integration.citationChanged.modified", fieldText)+"\n",
- DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) {
- throw new Zotero.Exception.UserCancelled("editing citation");
- }
+ + Zotero.getString("integration.citationChanged.original", this.properties.plainCitation)+"\n"
+ + Zotero.getString("integration.citationChanged.modified", fieldText)+"\n",
+ DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) {
+ throw new Zotero.Exception.UserCancelled("editing citation");
}
-
- // make sure it's going to get updated
- delete this.properties["formattedCitation"];
- delete this.properties["plainCitation"];
- delete this.properties["dontUpdate"];
-
- // Load items to be displayed in edit dialog
- yield this.loadItemData();
-
- }).apply(this, arguments);
+ }
+
+ // make sure it's going to get updated
+ delete this.properties["formattedCitation"];
+ delete this.properties["plainCitation"];
+ delete this.properties["dontUpdate"];
+
+ // Load items to be displayed in edit dialog
+ await this.loadItemData();
}
toJSON() {
@@ -2663,7 +2639,7 @@ Zotero.Integration.Bibliography = class {
Zotero.debug(`Integration: style.updateUncitedItems ${Array.from(this.uncitedItemIDs.values()).toSource()}`);
citeproc.updateUncitedItems(Array.from(this.uncitedItemIDs.values()));
- citeproc.setOutputFormat("rtf");
+ citeproc.setOutputFormat(Zotero.Integration.currentSession.outputFormat);
let bibliography = citeproc.makeBibliography();
Zotero.Cite.removeFromBibliography(bibliography, this.omittedItemIDs);
@@ -2725,3 +2701,119 @@ Zotero.Integration.Timer = class {
}
}
}
+
+Zotero.Integration.LegacyPluginWrapper = function(application) {
+ function wrapField(field) {
+ var wrapped = {rawField: field};
+ var fns = ['getNoteIndex', 'setCode', 'getCode', 'setText',
+ 'getText', 'removeCode', 'delete', 'select'];
+ for (let fn of fns) {
+ wrapped[fn] = async function() {
+ return field[fn].apply(field, arguments);
+ }
+ }
+ wrapped.equals = async function(other) {
+ return field.equals(other.rawField);
+ }
+ return wrapped;
+ }
+ function wrapDocument(doc) {
+ var wrapped = {};
+ var fns = ['complete', 'cleanup', 'setBibliographyStyle', 'setDocumentData',
+ 'getDocumentData', 'canInsertField', 'activate', 'displayAlert'];
+ for (let fn of fns) {
+ wrapped[fn] = async function() {
+ return doc[fn].apply(doc, arguments);
+ }
+ }
+ // Should return an async array
+ wrapped.getFields = async function(fieldType, progressCallback) {
+ if ('getFieldsAsync' in doc) {
+ var deferred = Zotero.Promise.defer();
+ var promise = deferred.promise;
+
+ var me = this;
+ doc.getFieldsAsync(fieldType,
+ {"observe":function(subject, topic, data) {
+ if(topic === "fields-available") {
+ if(progressCallback) {
+ try {
+ progressCallback(75);
+ } catch(e) {
+ Zotero.logError(e);
+ };
+ }
+
+ try {
+ // Add fields to fields array
+ var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator);
+ var fields = [];
+ while (fieldsEnumerator.hasMoreElements()) {
+ let field = fieldsEnumerator.getNext();
+ try {
+ fields.push(wrapField(field.QueryInterface(Components.interfaces.zoteroIntegrationField)));
+ } catch (e) {
+ fields.push(wrapField(field));
+ }
+ }
+ } catch(e) {
+ deferred.reject(e);
+ deferred = null;
+ return;
+ }
+
+ deferred.resolve(fields);
+ deferred = null;
+ } else if(topic === "fields-progress") {
+ if(progressCallback) {
+ try {
+ progressCallback((data ? parseInt(data, 10)*(3/4) : null));
+ } catch(e) {
+ Zotero.logError(e);
+ };
+ }
+ } else if(topic === "fields-error") {
+ deferred.reject(data);
+ deferred = null;
+ }
+ }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])});
+ return promise;
+ } else {
+ var result = doc.getFields.apply(doc, arguments);
+ var fields = [];
+ if (result.hasMoreElements) {
+ while (result.hasMoreElements()) {
+ fields.push(wrapField(result.getNext()));
+ await Zotero.Promise.delay();
+ }
+ } else {
+ fields = result;
+ }
+ return fields;
+ }
+ }
+ wrapped.insertField = async function() {
+ return wrapField(doc.insertField.apply(doc, arguments));
+ }
+ wrapped.cursorInField = async function() {
+ var result = doc.cursorInField.apply(doc, arguments);
+ return !result ? result : wrapField(result);
+ }
+ // Should take an arrayOfFields instead of an enumerator
+ wrapped.convert = async function(arrayOfFields) {
+ arguments[0] = new Zotero.Integration.JSEnumerator(arrayOfFields.map(f => f.rawField));
+ return doc.convert.apply(doc, arguments);
+ }
+ return wrapped;
+ }
+ return {
+ getDocument:
+ async function() {return wrapDocument(application.getDocument.apply(application, arguments))},
+ getActiveDocument:
+ async function() {return wrapDocument(application.getActiveDocument.apply(application, arguments))},
+ primaryFieldType: application.primaryFieldType,
+ secondaryFieldType: application.secondaryFieldType,
+ outputFormat: 'rtf',
+ supportedNotes: ['footnotes', 'endnotes']
+ }
+}
+\ No newline at end of file
diff --git a/chrome/content/zotero/xpcom/server.js b/chrome/content/zotero/xpcom/server.js
@@ -36,6 +36,7 @@ Zotero.Server = new function() {
412:"Precondition Failed",
500:"Internal Server Error",
501:"Not Implemented",
+ 503:"Service Unavailable",
504:"Gateway Timeout"
};
diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js
@@ -1,1276 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2011 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- http://zotero.org
-
- This file is part of Zotero.
-
- Zotero is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Zotero is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Zotero. If not, see <http://www.gnu.org/licenses/>.
-
- ***** END LICENSE BLOCK *****
-*/
-const CONNECTOR_API_VERSION = 2;
-
-Zotero.Server.Connector = {
- _waitingForSelection: {},
-
- getSaveTarget: function () {
- var zp = Zotero.getActiveZoteroPane(),
- library = null,
- collection = null,
- editable = true;
- try {
- library = Zotero.Libraries.get(zp.getSelectedLibraryID());
- collection = zp.getSelectedCollection();
- editable = zp.collectionsView.editable;
- }
- catch (e) {
- let id = Zotero.Prefs.get('lastViewedFolder');
- if (id) {
- ({ library, collection, editable } = this.resolveTarget(id));
- }
- }
-
- // Default to My Library if present if pane not yet opened
- // (which should never be the case anymore)
- if (!library) {
- let userLibrary = Zotero.Libraries.userLibrary;
- if (userLibrary) {
- library = userLibrary;
- }
- }
-
- return { library, collection, editable };
- },
-
- resolveTarget: function (targetID) {
- var library;
- var collection;
- var editable;
-
- var type = targetID[0];
- var id = parseInt(('' + targetID).substr(1));
-
- switch (type) {
- case 'L':
- library = Zotero.Libraries.get(id);
- editable = library.editable;
- break;
-
- case 'C':
- collection = Zotero.Collections.get(id);
- library = collection.library;
- editable = collection.editable;
- break;
-
- default:
- throw new Error(`Unsupported target type '${type}'`);
- }
-
- return { library, collection, editable };
- }
-};
-Zotero.Server.Connector.Data = {};
-
-Zotero.Server.Connector.SessionManager = {
- _sessions: new Map(),
-
- get: function (id) {
- return this._sessions.get(id);
- },
-
- create: function (id, action, requestData) {
- // Legacy connector
- if (!id) {
- Zotero.debug("No session id provided by client", 2);
- 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 = function (id, action, requestData) {
- this.id = id;
- this.created = new Date();
- this._action = action;
- this._requestData = requestData;
- this._items = new Set();
-};
-
-Zotero.Server.Connector.SaveSession.prototype.addItem = async function (item) {
- return this.addItems([item]);
-};
-
-Zotero.Server.Connector.SaveSession.prototype.addItems = async function (items) {
- for (let item of items) {
- this._items.add(item);
- }
-
- // Update the items with the current target data, in case it changed since the save began
- await this._updateItems(items);
-};
-
-/**
- * Change the target data for this session and update any items that have already been saved
- */
-Zotero.Server.Connector.SaveSession.prototype.update = async function (targetID, tags) {
- var previousTargetID = this._currentTargetID;
- this._currentTargetID = targetID;
- this._currentTags = tags || "";
-
- // Select new destination in collections pane
- var win = Zotero.getActiveZoteroPane();
- if (win && win.collectionsView) {
- await win.collectionsView.selectByID(targetID);
- }
- // If window is closed, select target collection re-open
- else {
- Zotero.Prefs.set('lastViewedFolder', targetID);
- }
-
- // If moving from a non-filesEditable library to a filesEditable library, resave from
- // original data, since there might be files that weren't saved or were removed
- if (previousTargetID && previousTargetID != targetID) {
- let { library: oldLibrary } = Zotero.Server.Connector.resolveTarget(previousTargetID);
- let { library: newLibrary } = Zotero.Server.Connector.resolveTarget(targetID);
- if (oldLibrary != newLibrary && !oldLibrary.filesEditable && newLibrary.filesEditable) {
- Zotero.debug("Resaving items to filesEditable library");
- if (this._action == 'saveItems' || this._action == 'saveSnapshot') {
- // Delete old items
- for (let item of this._items) {
- await item.eraseTx();
- }
- let actionUC = Zotero.Utilities.capitalize(this._action);
- let newItems = await Zotero.Server.Connector[actionUC].prototype[this._action](
- targetID, this._requestData
- );
- // saveSnapshot only returns a single item
- if (this._action == 'saveSnapshot') {
- newItems = [newItems];
- }
- this._items = new Set(newItems);
- }
- }
- }
-
- await this._updateItems(this._items);
-
- // If a single item was saved, select it (or its parent, if it now has one)
- if (win && win.collectionsView && this._items.size == 1) {
- let item = Array.from(this._items)[0];
- item = item.isTopLevelItem() ? item : item.parentItem;
- // Don't select if in trash
- if (!item.deleted) {
- await win.selectItem(item.id);
- }
- }
-};
-
-/**
- * Update the passed items with the current target and tags
- */
-Zotero.Server.Connector.SaveSession.prototype._updateItems = Zotero.serial(async function (items) {
- if (items.length == 0) {
- return;
- }
-
- var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
- var libraryID = library.libraryID;
-
- var tags = this._currentTags.trim();
- tags = tags ? tags.split(/\s*,\s*/) : [];
-
- Zotero.debug("Updating items for connector save session " + this.id);
-
- for (let item of items) {
- let newLibrary = Zotero.Libraries.get(library.libraryID);
-
- if (item.libraryID != libraryID) {
- let newItem = await item.moveToLibrary(libraryID);
- // Replace item in session
- this._items.delete(item);
- this._items.add(newItem);
- }
-
- // If the item is now a child item (e.g., from Retrieve Metadata for PDF), 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;
- }
- // 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();
-});
-
-
-Zotero.Server.Connector.SaveSession.prototype._updateRecents = function () {
- 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');
- }
-};
-
-
-Zotero.Server.Connector.AttachmentProgressManager = new function() {
- var attachmentsInProgress = new WeakMap(),
- attachmentProgress = {},
- id = 1;
-
- /**
- * Adds attachments to attachment progress manager
- */
- this.add = function(attachments) {
- for(var i=0; i<attachments.length; i++) {
- var attachment = attachments[i];
- attachmentsInProgress.set(attachment, (attachment.id = id++));
- }
- };
-
- /**
- * Called on attachment progress
- */
- this.onProgress = function(attachment, progress, error) {
- attachmentProgress[attachmentsInProgress.get(attachment)] = progress;
- };
-
- /**
- * Gets progress for a given progressID
- */
- this.getProgressForID = function(progressID) {
- return progressID in attachmentProgress ? attachmentProgress[progressID] : 0;
- };
-
- /**
- * Check if we have received progress for a given attachment
- */
- this.has = function(attachment) {
- return attachmentsInProgress.has(attachment)
- && attachmentsInProgress.get(attachment) in attachmentProgress;
- }
-};
-
-/**
- * Lists all available translators, including code for translators that should be run on every page
- *
- * Accepts:
- * Nothing
- * Returns:
- * Array of Zotero.Translator objects
- */
-Zotero.Server.Connector.GetTranslators = function() {};
-Zotero.Server.Endpoints["/connector/getTranslators"] = Zotero.Server.Connector.GetTranslators;
-Zotero.Server.Connector.GetTranslators.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Gets available translator list and other important data
- * @param {Object} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(data, sendResponseCallback) {
- // Translator data
- var me = this;
- if(data.url) {
- Zotero.Translators.getWebTranslatorsForLocation(data.url, data.rootUrl).then(function(data) {
- sendResponseCallback(200, "application/json",
- JSON.stringify(me._serializeTranslators(data[0])));
- });
- } else {
- Zotero.Translators.getAll().then(function(translators) {
- var responseData = me._serializeTranslators(translators);
- sendResponseCallback(200, "application/json", JSON.stringify(responseData));
- }).catch(function(e) {
- sendResponseCallback(500);
- throw e;
- }).done();
- }
- },
-
- _serializeTranslators: function(translators) {
- var responseData = [];
- let properties = ["translatorID", "translatorType", "label", "creator", "target", "targetAll",
- "minVersion", "maxVersion", "priority", "browserSupport", "inRepository", "lastUpdated"];
- for (var translator of translators) {
- responseData.push(translator.serialize(properties));
- }
- return responseData;
- }
-}
-
-/**
- * Detects whether there is an available translator to handle a given page
- *
- * Accepts:
- * uri - The URI of the page to be saved
- * html - document.innerHTML or equivalent
- * cookie - document.cookie or equivalent
- *
- * Returns a list of available translators as an array
- */
-Zotero.Server.Connector.Detect = function() {};
-Zotero.Server.Endpoints["/connector/detect"] = Zotero.Server.Connector.Detect;
-Zotero.Server.Connector.Detect.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Loads HTML into a hidden browser and initiates translator detection
- * @param {Object} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(url, data, sendResponseCallback) {
- this.sendResponse = sendResponseCallback;
- this._parsedPostData = data;
-
- this._translate = new Zotero.Translate("web");
- this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) });
-
- Zotero.Server.Connector.Data[this._parsedPostData["uri"]] = "<html>"+this._parsedPostData["html"]+"</html>";
- this._browser = Zotero.Browser.createHiddenBrowser();
-
- var ioService = Components.classes["@mozilla.org/network/io-service;1"]
- .getService(Components.interfaces.nsIIOService);
- var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null);
-
- var pageShowCalled = false;
- var me = this;
- this._translate.setCookieSandbox(new Zotero.CookieSandbox(this._browser,
- this._parsedPostData["uri"], this._parsedPostData["cookie"], url.userAgent));
- this._browser.addEventListener("DOMContentLoaded", function() {
- try {
- if(me._browser.contentDocument.location.href == "about:blank") return;
- if(pageShowCalled) return;
- pageShowCalled = true;
- delete Zotero.Server.Connector.Data[me._parsedPostData["uri"]];
-
- // get translators
- me._translate.setDocument(me._browser.contentDocument);
- me._translate.setLocation(me._parsedPostData["uri"], me._parsedPostData["uri"]);
- me._translate.getTranslators();
- } catch(e) {
- sendResponseCallback(500);
- throw e;
- }
- }, false);
-
- me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"]));
- },
-
- /**
- * Callback to be executed when list of translators becomes available. Sends standard
- * translator passing properties with proxies where available for translators.
- * @param {Zotero.Translate} translate
- * @param {Zotero.Translator[]} translators
- */
- _translatorsAvailable: function(translate, translators) {
- translators = translators.map(function(translator) {
- translator = translator.serialize(TRANSLATOR_PASSING_PROPERTIES.concat('proxy'));
- translator.proxy = translator.proxy ? translator.proxy.toJSON() : null;
- return translator;
- });
- this.sendResponse(200, "application/json", JSON.stringify(translators));
-
- Zotero.Browser.deleteHiddenBrowser(this._browser);
- }
-}
-
-/**
- * Performs translation of a given page
- *
- * Accepts:
- * uri - The URI of the page to be saved
- * html - document.innerHTML or equivalent
- * cookie - document.cookie or equivalent
- * translatorID [optional] - a translator ID as returned by /connector/detect
- *
- * Returns:
- * If a single item, sends response code 201 with item in body.
- * If multiple items, sends response code 300 with the following content:
- * items - list of items in the format typically passed to the selectItems handler
- * instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call
- * uri - the URI of the page for which multiple items are available
- */
-Zotero.Server.Connector.SavePage = function() {};
-Zotero.Server.Endpoints["/connector/savePage"] = Zotero.Server.Connector.SavePage;
-Zotero.Server.Connector.SavePage.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Either loads HTML into a hidden browser and initiates translation, or saves items directly
- * to the database
- * @param {Object} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(url, data, sendResponseCallback) {
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- if (!library.editable) {
- Zotero.logError("Can't add item to read-only library " + library.name);
- return sendResponseCallback(500, "application/json", JSON.stringify({ libraryEditable: false }));
- }
-
- this.sendResponse = sendResponseCallback;
- Zotero.Server.Connector.Detect.prototype.init.apply(this, [url, data, sendResponseCallback])
- },
-
- /**
- * Callback to be executed when items must be selected
- * @param {Zotero.Translate} translate
- * @param {Object} itemList ID=>text pairs representing available items
- */
- _selectItems: function(translate, itemList, callback) {
- var instanceID = Zotero.randomString();
- Zotero.Server.Connector._waitingForSelection[instanceID] = this;
-
- // Fix for translators that don't create item lists as objects
- if(itemList.push && typeof itemList.push === "function") {
- var newItemList = {};
- for(var item in itemList) {
- newItemList[item] = itemList[item];
- }
- itemList = newItemList;
- }
-
- // Send "Multiple Choices" HTTP response
- this.sendResponse(300, "application/json", JSON.stringify({selectItems: itemList, instanceID: instanceID, uri: this._parsedPostData.uri}));
- this.selectedItemsCallback = callback;
- },
-
- /**
- * Callback to be executed when list of translators becomes available. Opens progress window,
- * selects specified translator, and initiates translation.
- * @param {Zotero.Translate} translate
- * @param {Zotero.Translator[]} translators
- */
- _translatorsAvailable: function(translate, translators) {
- // make sure translatorsAvailable succeded
- if(!translators.length) {
- Zotero.Browser.deleteHiddenBrowser(this._browser);
- this.sendResponse(500);
- return;
- }
-
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- var libraryID = library.libraryID;
-
- // set handlers for translation
- var me = this;
- var jsonItems = [];
- translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) });
- translate.setHandler("itemDone", function(obj, item, jsonItem) {
- Zotero.Server.Connector.AttachmentProgressManager.add(jsonItem.attachments);
- jsonItems.push(jsonItem);
- });
- translate.setHandler("attachmentProgress", function(obj, attachment, progress, error) {
- Zotero.Server.Connector.AttachmentProgressManager.onProgress(attachment, progress, error);
- });
- translate.setHandler("done", function(obj, item) {
- Zotero.Browser.deleteHiddenBrowser(me._browser);
- if(jsonItems.length || me.selectedItems === false) {
- me.sendResponse(201, "application/json", JSON.stringify({items: jsonItems}));
- } else {
- me.sendResponse(500);
- }
- });
-
- if (this._parsedPostData.translatorID) {
- translate.setTranslator(this._parsedPostData.translatorID);
- } else {
- translate.setTranslator(translators[0]);
- }
- translate.translate({libraryID, collections: collection ? [collection.id] : false});
- }
-}
-
-/**
- * Saves items to DB
- *
- * Accepts:
- * items - an array of JSON format items
- * Returns:
- * 201 response code with item in body.
- */
-Zotero.Server.Connector.SaveItems = function() {};
-Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItems;
-Zotero.Server.Connector.SaveItems.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Either loads HTML into a hidden browser and initiates translation, or saves items directly
- * to the database
- */
- init: Zotero.Promise.coroutine(function* (requestData) {
- var data = requestData.data;
-
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- var libraryID = library.libraryID;
- var targetID = collection ? collection.treeViewID : library.treeViewID;
-
- try {
- var session = Zotero.Server.Connector.SessionManager.create(
- data.sessionID,
- 'saveItems',
- requestData
- );
- }
- catch (e) {
- return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
- }
- yield session.update(targetID);
-
- // TODO: Default to My Library root, since it's changeable
- if (!library.editable) {
- Zotero.logError("Can't add item to read-only library " + library.name);
- return [500, "application/json", JSON.stringify({ libraryEditable: false })];
- }
-
- return new Zotero.Promise((resolve) => {
- try {
- this.saveItems(
- targetID,
- requestData,
- function (topLevelItems) {
- resolve([201, "application/json", JSON.stringify({items: topLevelItems})]);
- }
- )
- // Add items to session once all attachments have been saved
- .then(function (items) {
- session.addItems(items);
- });
- }
- catch (e) {
- Zotero.logError(e);
- resolve(500);
- }
- });
- }),
-
- saveItems: async function (target, requestData, onTopLevelItemsDone) {
- var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
-
- var data = requestData.data;
- var cookieSandbox = data.uri
- ? new Zotero.CookieSandbox(
- null,
- data.uri,
- data.detailedCookies ? "" : data.cookie || "",
- requestData.headers["User-Agent"]
- )
- : null;
- if (cookieSandbox && data.detailedCookies) {
- cookieSandbox.addCookiesFromHeader(data.detailedCookies);
- }
-
- for (let item of data.items) {
- Zotero.Server.Connector.AttachmentProgressManager.add(item.attachments);
- }
-
- var proxy = data.proxy && new Zotero.Proxy(data.proxy);
-
- // Save items
- var itemSaver = new Zotero.Translate.ItemSaver({
- libraryID: library.libraryID,
- collections: collection ? [collection.id] : undefined,
- attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD,
- forceTagType: 1,
- referrer: data.uri,
- cookieSandbox,
- proxy
- });
- return itemSaver.saveItems(
- data.items,
- Zotero.Server.Connector.AttachmentProgressManager.onProgress,
- function () {
- // Remove attachments from item.attachments that aren't being saved. We have to
- // clone the items so that we don't mutate the data stored in the session.
- var savedItems = [...data.items.map(item => Object.assign({}, item))];
- for (let item of savedItems) {
- item.attachments = item.attachments
- .filter(attachment => {
- return Zotero.Server.Connector.AttachmentProgressManager.has(attachment);
- });
- }
- if (onTopLevelItemsDone) {
- onTopLevelItemsDone(savedItems);
- }
- }
- );
- }
-}
-
-/**
- * Saves a snapshot to the DB
- *
- * Accepts:
- * uri - The URI of the page to be saved
- * html - document.innerHTML or equivalent
- * cookie - document.cookie or equivalent
- * Returns:
- * Nothing (200 OK response)
- */
-Zotero.Server.Connector.SaveSnapshot = function() {};
-Zotero.Server.Endpoints["/connector/saveSnapshot"] = Zotero.Server.Connector.SaveSnapshot;
-Zotero.Server.Connector.SaveSnapshot.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Save snapshot
- */
- init: async function (requestData) {
- var data = requestData.data;
-
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- var targetID = collection ? collection.treeViewID : library.treeViewID;
-
- try {
- var session = Zotero.Server.Connector.SessionManager.create(
- data.sessionID,
- 'saveSnapshot',
- requestData
- );
- }
- catch (e) {
- return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
- }
- await session.update(collection ? collection.treeViewID : library.treeViewID);
-
- // TODO: Default to My Library root, since it's changeable
- if (!library.editable) {
- Zotero.logError("Can't add item to read-only library " + library.name);
- return [500, "application/json", JSON.stringify({ libraryEditable: false })];
- }
-
- try {
- let item = await this.saveSnapshot(targetID, requestData);
- await session.addItem(item);
- }
- catch (e) {
- Zotero.logError(e);
- return 500;
- }
-
- return 201;
- },
-
- saveSnapshot: async function (target, requestData) {
- var { library, collection, editable } = Zotero.Server.Connector.resolveTarget(target);
- var libraryID = library.libraryID;
- var data = requestData.data;
-
- var cookieSandbox = data.url
- ? new Zotero.CookieSandbox(
- null,
- data.url,
- data.detailedCookies ? "" : data.cookie || "",
- requestData.headers["User-Agent"]
- )
- : null;
- if (cookieSandbox && data.detailedCookies) {
- cookieSandbox.addCookiesFromHeader(data.detailedCookies);
- }
-
- if (data.pdf && library.filesEditable) {
- let item = await Zotero.Attachments.importFromURL({
- libraryID,
- url: data.url,
- collections: collection ? [collection.id] : undefined,
- contentType: "application/pdf",
- cookieSandbox
- });
-
- // Automatically recognize PDF
- Zotero.RecognizePDF.autoRecognizeItems([item]);
-
- return item;
- }
-
- return new Zotero.Promise((resolve, reject) => {
- Zotero.Server.Connector.Data[data.url] = "<html>" + data.html + "</html>";
- Zotero.HTTP.loadDocuments(
- ["zotero://connector/" + encodeURIComponent(data.url)],
- async function (doc) {
- delete Zotero.Server.Connector.Data[data.url];
-
- try {
- // Create new webpage item
- let item = new Zotero.Item("webpage");
- item.libraryID = libraryID;
- item.setField("title", doc.title);
- item.setField("url", data.url);
- item.setField("accessDate", "CURRENT_TIMESTAMP");
- if (collection) {
- item.setCollections([collection.id]);
- }
- var itemID = await item.saveTx();
-
- // Save snapshot
- if (library.filesEditable && !data.skipSnapshot) {
- await Zotero.Attachments.importFromDocument({
- document: doc,
- parentItemID: itemID
- });
- }
-
- resolve(item);
- }
- catch (e) {
- reject(e);
- }
- },
- null,
- null,
- false,
- cookieSandbox
- );
- });
- }
-}
-
-/**
- * Handle item selection
- *
- * Accepts:
- * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler
- * instanceID - as returned by savePage call
- * Returns:
- * 201 response code with empty body
- */
-Zotero.Server.Connector.SelectItems = function() {};
-Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems;
-Zotero.Server.Connector.SelectItems.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Finishes up translation when item selection is complete
- * @param {String} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(data, sendResponseCallback) {
- var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID];
- saveInstance.sendResponse = sendResponseCallback;
-
- var selectedItems = false;
- for(var i in data.selectedItems) {
- selectedItems = data.selectedItems;
- break;
- }
- saveInstance.selectedItemsCallback(selectedItems);
- }
-}
-
-/**
- *
- *
- * Accepts:
- * sessionID - A session ID previously passed to /saveItems
- * target - A treeViewID (L1, C23, etc.) for the library or collection to save to
- * tags - A string of tags separated by commas
- *
- * Returns:
- * 200 response on successful change
- * 400 on error with 'error' property in JSON
- */
-Zotero.Server.Connector.UpdateSession = function() {};
-Zotero.Server.Endpoints["/connector/updateSession"] = Zotero.Server.Connector.UpdateSession;
-Zotero.Server.Connector.UpdateSession.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- init: async function (requestData) {
- var data = requestData.data
-
- if (!data.sessionID) {
- return [400, "application/json", JSON.stringify({ error: "SESSION_ID_NOT_PROVIDED" })];
- }
-
- var session = Zotero.Server.Connector.SessionManager.get(data.sessionID);
- if (!session) {
- Zotero.debug("Can't find session " + data.sessionID, 1);
- return [400, "application/json", JSON.stringify({ error: "SESSION_NOT_FOUND" })];
- }
-
- // Parse treeViewID
- var [type, id] = [data.target[0], parseInt(data.target.substr(1))];
- var tags = data.tags;
-
- if (type == 'C') {
- let collection = await Zotero.Collections.getAsync(id);
- if (!collection) {
- return [400, "application/json", JSON.stringify({ error: "COLLECTION_NOT_FOUND" })];
- }
- }
-
- await session.update(data.target, tags);
-
- return [200, "application/json", JSON.stringify({})];
- }
-};
-
-Zotero.Server.Connector.DelaySync = function () {};
-Zotero.Server.Endpoints["/connector/delaySync"] = Zotero.Server.Connector.DelaySync;
-Zotero.Server.Connector.DelaySync.prototype = {
- supportedMethods: ["POST"],
-
- init: async function (requestData) {
- Zotero.Sync.Runner.delaySync(10000);
- return [204];
- }
-};
-
-/**
- * Gets progress for an attachment that is currently being saved
- *
- * Accepts:
- * Array of attachment IDs returned by savePage, saveItems, or saveSnapshot
- * Returns:
- * 200 response code with current progress in body. Progress is either a number
- * between 0 and 100 or "false" to indicate that saving failed.
- */
-Zotero.Server.Connector.Progress = function() {};
-Zotero.Server.Endpoints["/connector/attachmentProgress"] = Zotero.Server.Connector.Progress;
-Zotero.Server.Connector.Progress.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * @param {String} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(data, sendResponseCallback) {
- sendResponseCallback(200, "application/json",
- JSON.stringify(data.map(id => Zotero.Server.Connector.AttachmentProgressManager.getProgressForID(id))));
- }
-};
-
-/**
- * Translates resources using import translators
- *
- * Returns:
- * - Object[Item] an array of imported items
- */
-
-Zotero.Server.Connector.Import = function() {};
-Zotero.Server.Endpoints["/connector/import"] = Zotero.Server.Connector.Import;
-Zotero.Server.Connector.Import.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: '*',
- permitBookmarklet: false,
-
- init: async function (requestData) {
- let translate = new Zotero.Translate.Import();
- translate.setString(requestData.data);
- let translators = await translate.getTranslators();
- if (!translators || !translators.length) {
- return 400;
- }
- translate.setTranslator(translators[0]);
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- var libraryID = library.libraryID;
- if (!library.editable) {
- Zotero.logError("Can't import into read-only library " + library.name);
- return [500, "application/json", JSON.stringify({ libraryEditable: false })];
- }
-
- try {
- var session = Zotero.Server.Connector.SessionManager.create(requestData.query.session);
- }
- catch (e) {
- return [409, "application/json", JSON.stringify({ error: "SESSION_EXISTS" })];
- }
- await session.update(collection ? collection.treeViewID : library.treeViewID);
-
- let items = await translate.translate({
- libraryID,
- collections: collection ? [collection.id] : null,
- // Import translation skips selection by default, so force it to occur
- saveOptions: {
- skipSelect: false
- }
- });
- session.addItems(items);
-
- return [201, "application/json", JSON.stringify(items)];
- }
-}
-
-/**
- * Install CSL styles
- *
- * Returns:
- * - {name: styleName}
- */
-
-Zotero.Server.Connector.InstallStyle = function() {};
-Zotero.Server.Endpoints["/connector/installStyle"] = Zotero.Server.Connector.InstallStyle;
-Zotero.Server.Connector.InstallStyle.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: '*',
- permitBookmarklet: false,
-
- init: Zotero.Promise.coroutine(function* (requestData) {
- try {
- var styleName = yield Zotero.Styles.install(
- requestData.data, requestData.query.origin || null, true
- );
- } catch (e) {
- return [400, "text/plain", e.message];
- }
- return [201, "application/json", JSON.stringify({name: styleName})];
- })
-};
-
-/**
- * Get code for a translator
- *
- * Accepts:
- * translatorID
- * Returns:
- * code - translator code
- */
-Zotero.Server.Connector.GetTranslatorCode = function() {};
-Zotero.Server.Endpoints["/connector/getTranslatorCode"] = Zotero.Server.Connector.GetTranslatorCode;
-Zotero.Server.Connector.GetTranslatorCode.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Returns a 200 response to say the server is alive
- * @param {String} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(postData, sendResponseCallback) {
- var translator = Zotero.Translators.get(postData.translatorID);
- translator.getCode().then(function(code) {
- sendResponseCallback(200, "application/javascript", code);
- });
- }
-}
-
-/**
- * Get selected collection
- *
- * Accepts:
- * Nothing
- * Returns:
- * libraryID
- * libraryName
- * collectionID
- * collectionName
- */
-Zotero.Server.Connector.GetSelectedCollection = function() {};
-Zotero.Server.Endpoints["/connector/getSelectedCollection"] = Zotero.Server.Connector.GetSelectedCollection;
-Zotero.Server.Connector.GetSelectedCollection.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- /**
- * Returns a 200 response to say the server is alive
- * @param {String} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(postData, sendResponseCallback) {
- var { library, collection, editable } = Zotero.Server.Connector.getSaveTarget();
- var response = {
- libraryID: library.libraryID,
- libraryName: library.name,
- libraryEditable: library.editable,
- editable
- };
-
- if(collection && collection.id) {
- response.id = collection.id;
- response.name = collection.name;
- } else {
- response.id = null;
- response.name = response.libraryName;
- }
-
- // Get list of editable libraries and collections
- var collections = [];
- var originalLibraryID = library.libraryID;
- for (let library of Zotero.Libraries.getAll()) {
- if (!library.editable) continue;
-
- // Add recent: true for recent targets
-
- collections.push(
- {
- id: library.treeViewID,
- name: library.name
- },
- ...Zotero.Collections.getByLibrary(library.libraryID, true).map(c => ({
- id: c.treeViewID,
- name: c.name,
- level: c.level + 1 || 1 // Added by Zotero.Collections._getByContainer()
- }))
- );
- }
- response.targets = collections;
-
- // Mark recent targets
- try {
- let recents = Zotero.Prefs.get('recentSaveTargets');
- if (recents) {
- recents = new Set(JSON.parse(recents).map(o => o.id));
- for (let target of response.targets) {
- if (recents.has(target.id)) {
- target.recent = true;
- }
- }
- }
- }
- catch (e) {
- Zotero.logError(e);
- Zotero.Prefs.clear('recentSaveTargets');
- }
-
- // TODO: Limit debug size
- sendResponseCallback(200, "application/json", JSON.stringify(response));
- }
-}
-
-/**
- * Get a list of client hostnames (reverse local IP DNS)
- *
- * Accepts:
- * Nothing
- * Returns:
- * {Array} hostnames
- */
-Zotero.Server.Connector.GetClientHostnames = {};
-Zotero.Server.Connector.GetClientHostnames = function() {};
-Zotero.Server.Endpoints["/connector/getClientHostnames"] = Zotero.Server.Connector.GetClientHostnames;
-Zotero.Server.Connector.GetClientHostnames.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: false,
-
- /**
- * Returns a 200 response to say the server is alive
- */
- init: Zotero.Promise.coroutine(function* (requestData) {
- try {
- var hostnames = yield Zotero.Proxies.DNS.getHostnames();
- } catch(e) {
- return 500;
- }
- return [200, "application/json", JSON.stringify(hostnames)];
- })
-};
-
-/**
- * Get a list of stored proxies
- *
- * Accepts:
- * Nothing
- * Returns:
- * {Array} hostnames
- */
-Zotero.Server.Connector.Proxies = {};
-Zotero.Server.Connector.Proxies = function() {};
-Zotero.Server.Endpoints["/connector/proxies"] = Zotero.Server.Connector.Proxies;
-Zotero.Server.Connector.Proxies.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: false,
-
- /**
- * Returns a 200 response to say the server is alive
- */
- init: Zotero.Promise.coroutine(function* () {
- let proxies = Zotero.Proxies.proxies.map((p) => Object.assign(p.toJSON(), {hosts: p.hosts}));
- return [200, "application/json", JSON.stringify(proxies)];
- })
-};
-
-
-/**
- * Test connection
- *
- * Accepts:
- * Nothing
- * Returns:
- * Nothing (200 OK response)
- */
-Zotero.Server.Connector.Ping = function() {};
-Zotero.Server.Endpoints["/connector/ping"] = Zotero.Server.Connector.Ping;
-Zotero.Server.Connector.Ping.prototype = {
- supportedMethods: ["GET", "POST"],
- supportedDataTypes: ["application/json", "text/plain"],
- permitBookmarklet: true,
-
- /**
- * Sends 200 and HTML status on GET requests
- * @param data {Object} request information defined in connector.js
- */
- init: function (req) {
- if (req.method == 'GET') {
- return [200, "text/html", '<!DOCTYPE html><html><head>' +
- '<title>Zotero Connector Server is Available</title></head>' +
- '<body>Zotero Connector Server is Available</body></html>'];
- } else {
- // Store the active URL so it can be used for site-specific Quick Copy
- if (req.data.activeURL) {
- //Zotero.debug("Setting active URL to " + req.data.activeURL);
- Zotero.QuickCopy.lastActiveURL = req.data.activeURL;
- }
-
- let response = {
- prefs: {
- automaticSnapshots: Zotero.Prefs.get('automaticSnapshots')
- }
- };
- if (Zotero.QuickCopy.hasSiteSettings()) {
- response.prefs.reportActiveURL = true;
- }
-
- return [200, 'application/json', JSON.stringify(response)];
- }
- }
-}
-
-/**
- * IE messaging hack
- *
- * Accepts:
- * Nothing
- * Returns:
- * Static Response
- */
-Zotero.Server.Connector.IEHack = function() {};
-Zotero.Server.Endpoints["/connector/ieHack"] = Zotero.Server.Connector.IEHack;
-Zotero.Server.Connector.IEHack.prototype = {
- supportedMethods: ["GET"],
- permitBookmarklet: true,
-
- /**
- * Sends a fixed webpage
- * @param {String} data POST data or GET query string
- * @param {Function} sendResponseCallback function to send HTTP response
- */
- init: function(postData, sendResponseCallback) {
- sendResponseCallback(200, "text/html",
- '<!DOCTYPE html><html><head>'+
- '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'common_ie.js"></script>'+
- '<script src="'+ZOTERO_CONFIG.BOOKMARKLET_URL+'ie_hack.js"></script>'+
- '</head><body></body></html>');
- }
-}
-
-// XXX For compatibility with older connectors; to be removed
-Zotero.Server.Connector.IncompatibleVersion = function() {};
-Zotero.Server.Connector.IncompatibleVersion._errorShown = false
-Zotero.Server.Endpoints["/translate/list"] = Zotero.Server.Connector.IncompatibleVersion;
-Zotero.Server.Endpoints["/translate/detect"] = Zotero.Server.Connector.IncompatibleVersion;
-Zotero.Server.Endpoints["/translate/save"] = Zotero.Server.Connector.IncompatibleVersion;
-Zotero.Server.Endpoints["/translate/select"] = Zotero.Server.Connector.IncompatibleVersion;
-Zotero.Server.Connector.IncompatibleVersion.prototype = {
- supportedMethods: ["POST"],
- supportedDataTypes: ["application/json"],
- permitBookmarklet: true,
-
- init: function(postData, sendResponseCallback) {
- sendResponseCallback(404);
- if(Zotero.Server.Connector.IncompatibleVersion._errorShown) return;
-
- Zotero.Utilities.Internal.activate();
- var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
- createInstance(Components.interfaces.nsIPromptService);
- ps.alert(null,
- Zotero.getString("connector.error.title"),
- Zotero.getString("integration.error.incompatibleVersion2",
- ["Standalone "+Zotero.version, "Connector", "2.999.1"]));
- Zotero.Server.Connector.IncompatibleVersion._errorShown = true;
- }
-};
-\ No newline at end of file
diff --git a/components/zotero-service.js b/components/zotero-service.js
@@ -132,18 +132,9 @@ const xpcomFilesLocal = [
'users',
'translation/translate_item',
'translation/translators',
- 'server_connector'
-];
-
-/** XPCOM files to be loaded only for connector translation and DB access **/
-const xpcomFilesConnector = [
- 'connector/translate_item',
- 'connector/translator',
- 'connector/connector',
- 'connector/connector_firefox',
- 'connector/cachedTypes',
- 'connector/repo',
- 'connector/typeSchemaData'
+ 'connector/httpIntegrationClient',
+ 'connector/server_connector',
+ 'connector/server_connectorIntegration',
];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js
@@ -7,6 +7,9 @@ describe("Zotero.Integration", function () {
const INTEGRATION_TYPE_TEMP = 3;
/**
* To be used as a reference for Zotero-Word Integration plugins
+ *
+ * NOTE: Functions must return promises instead of values!
+ * The functions defined for the dummy are promisified below
*/
var DocumentPluginDummy = {};
@@ -17,6 +20,7 @@ describe("Zotero.Integration", function () {
this.doc = new DocumentPluginDummy.Document();
this.primaryFieldType = "Field";
this.secondaryFieldType = "Bookmark";
+ this.supportedNotes = ['footnotes', 'endnotes'];
this.fields = [];
};
DocumentPluginDummy.Application.prototype = {
@@ -87,26 +91,15 @@ describe("Zotero.Integration", function () {
throw new Error("noteType must be an integer");
}
var field = new DocumentPluginDummy.Field(this);
- this.fields.push(field);
- return field
+ this.fields.push(field);
+ return field;
},
/**
* Gets all fields present in the document.
* @param {String} fieldType
- * @returns {DocumentPluginDummy.FieldEnumerator}
+ * @returns {DocumentPluginDummy.Field[]}
*/
- getFields: function(fieldType) {return new DocumentPluginDummy.FieldEnumerator(this)},
- /**
- * Gets all fields present in the document. The observer will receive notifications for two
- * topics: "fields-progress", with the document as the subject and percent progress as data, and
- * "fields-available", with an nsISimpleEnumerator of fields as the subject and the length as
- * data
- * @param {String} fieldType
- * @param {nsIObserver} observer
- */
- getFieldsAsync: function(fieldType, observer) {
- observer.observe(this.getFields(fieldType), 'fields-available', null)
- },
+ getFields: function(fieldType) {return Array.from(this.fields)},
/**
* Sets the bibliography style, overwriting the current values for this document
*/
@@ -114,7 +107,7 @@ describe("Zotero.Integration", function () {
tabStops, tabStopsCount) => 0,
/**
* Converts all fields in a document to a different fieldType or noteType
- * @params {DocumentPluginDummy.FieldEnumerator} fields
+ * @params {DocumentPluginDummy.Field[]} fields
*/
convert: (fields, toFieldType, toNoteType, count) => 0,
/**
@@ -128,14 +121,6 @@ describe("Zotero.Integration", function () {
complete: () => 0,
};
- DocumentPluginDummy.FieldEnumerator = function(doc) {this.doc = doc; this.idx = 0};
- DocumentPluginDummy.FieldEnumerator.prototype = {
- hasMoreElements: function() {return this.idx < this.doc.fields.length;},
- getNext: function() {return this.doc.fields[this.idx++]},
- QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports,
- Components.interfaces.nsISimpleEnumerator])
- };
-
/**
* The Field class corresponds to a field containing an individual citation
* or bibliography
@@ -197,11 +182,12 @@ describe("Zotero.Integration", function () {
getNoteIndex: () => 0,
};
- for (let cls of ['Application', 'Document', 'FieldEnumerator', 'Field']) {
+ // Processing functions for logging and promisification
+ for (let cls of ['Application', 'Document', 'Field']) {
for (let methodName in DocumentPluginDummy[cls].prototype) {
if (methodName !== 'QueryInterface') {
let method = DocumentPluginDummy[cls].prototype[methodName];
- DocumentPluginDummy[cls].prototype[methodName] = function() {
+ DocumentPluginDummy[cls].prototype[methodName] = async function() {
try {
Zotero.debug(`DocumentPluginDummy: ${cls}.${methodName} invoked with args ${JSON.stringify(arguments)}`, 2);
} catch (e) {
@@ -250,7 +236,7 @@ describe("Zotero.Integration", function () {
editBibliographyDialog: {}
};
- function initDoc(docID, options={}) {
+ async function initDoc(docID, options={}) {
applications[docID] = new DocumentPluginDummy.Application();
var data = new Zotero.Integration.DocumentData();
data.prefs = {
@@ -261,7 +247,7 @@ describe("Zotero.Integration", function () {
data.style = {styleID, locale: 'en-US', hasBibliography: true, bibliographyStyleHasBeenSet: true};
data.sessionID = Zotero.Utilities.randomString(10);
Object.assign(data, options);
- applications[docID].getActiveDocument().setDocumentData(data.serialize());
+ await (await applications[docID].getDocument(docID)).setDocumentData(data.serialize());
}
function setDefaultIntegrationDocPrefs() {
@@ -354,10 +340,10 @@ describe("Zotero.Integration", function () {
var displayAlertStub;
var style;
before(function* () {
- displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').returns(0);
+ displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').resolves(0);
});
- beforeEach(function() {
+ beforeEach(async function () {
// 🦉birds?
style = {styleID: "http://www.example.com/csl/waterbirds", locale: 'en-US'};
@@ -365,7 +351,7 @@ describe("Zotero.Integration", function () {
try {
Zotero.Styles.get(style.styleID).remove();
} catch (e) {}
- initDoc(docID, {style});
+ await initDoc(docID, {style});
displayDialogStub.resetHistory();
displayAlertStub.reset();
});
@@ -386,7 +372,7 @@ describe("Zotero.Integration", function () {
}
return style;
});
- displayAlertStub.returns(1);
+ displayAlertStub.resolves(1);
yield execCommand('addEditCitation', docID);
assert.isTrue(displayAlertStub.calledOnce);
assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul'));
@@ -397,7 +383,7 @@ describe("Zotero.Integration", function () {
});
it('should prompt with the document preferences dialog if user clicks NO', function* () {
- displayAlertStub.returns(0);
+ displayAlertStub.resolves(0);
yield execCommand('addEditCitation', docID);
assert.isTrue(displayAlertStub.calledOnce);
// Prefs to select a new style and quickFormat
@@ -407,7 +393,7 @@ describe("Zotero.Integration", function () {
});
it('should download the style without prompting if it is from zotero.org', function* (){
- initDoc(docID, {styleID: "http://www.zotero.org/styles/waterbirds", locale: 'en-US'});
+ yield initDoc(docID, {styleID: "http://www.zotero.org/styles/waterbirds", locale: 'en-US'});
var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves();
var style = Zotero.Styles.get(styleID);
var styleGetCalledOnce = false;
@@ -418,7 +404,7 @@ describe("Zotero.Integration", function () {
}
return style;
});
- displayAlertStub.returns(1);
+ displayAlertStub.resolves(1);
yield execCommand('addEditCitation', docID);
assert.isFalse(displayAlertStub.called);
assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul'));
@@ -433,20 +419,20 @@ describe("Zotero.Integration", function () {
describe('#addEditCitation', function() {
var insertMultipleCitations = Zotero.Promise.coroutine(function *() {
var docID = this.test.fullTitle();
- if (!(docID in applications)) initDoc(docID);
+ if (!(docID in applications)) yield initDoc(docID);
var doc = applications[docID].doc;
setAddEditItems(testItems[0]);
yield execCommand('addEditCitation', docID);
assert.equal(doc.fields.length, 1);
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = yield (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.equal(citation.citationItems.length, 1);
assert.equal(citation.citationItems[0].id, testItems[0].id);
setAddEditItems(testItems.slice(1, 3));
yield execCommand('addEditCitation', docID);
assert.equal(doc.fields.length, 2);
- citation = (new Zotero.Integration.CitationField(doc.fields[1], doc.fields[1].code)).unserialize();
+ citation = yield (new Zotero.Integration.CitationField(doc.fields[1], doc.fields[1].code)).unserialize();
assert.equal(citation.citationItems.length, 2);
for (let i = 1; i < 3; i++) {
assert.equal(citation.citationItems[i-1].id, testItems[i].id);
@@ -459,13 +445,13 @@ describe("Zotero.Integration", function () {
var docID = this.test.fullTitle();
var doc = applications[docID].doc;
- sinon.stub(doc, 'cursorInField').returns(doc.fields[0]);
- sinon.stub(doc, 'canInsertField').returns(false);
+ sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]);
+ sinon.stub(doc, 'canInsertField').resolves(false);
setAddEditItems(testItems.slice(3, 5));
yield execCommand('addEditCitation', docID);
assert.equal(doc.fields.length, 2);
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = yield (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.equal(citation.citationItems.length, 2);
assert.equal(citation.citationItems[0].id, testItems[3].id);
});
@@ -494,7 +480,7 @@ describe("Zotero.Integration", function () {
describe('when original citation text has been modified', function() {
var displayAlertStub;
before(function* () {
- displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').returns(0);
+ displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').resolves(0);
});
beforeEach(function() {
displayAlertStub.reset();
@@ -508,8 +494,8 @@ describe("Zotero.Integration", function () {
var doc = applications[docID].doc;
doc.fields[0].text = "modified";
- sinon.stub(doc, 'cursorInField').returns(doc.fields[0]);
- sinon.stub(doc, 'canInsertField').returns(false);
+ sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]);
+ sinon.stub(doc, 'canInsertField').resolves(false);
await execCommand('addEditCitation', docID);
assert.equal(doc.fields.length, 2);
@@ -523,9 +509,9 @@ describe("Zotero.Integration", function () {
let origText = doc.fields[0].text;
doc.fields[0].text = "modified";
// Return OK
- displayAlertStub.returns(1);
- sinon.stub(doc, 'cursorInField').returns(doc.fields[0]);
- sinon.stub(doc, 'canInsertField').returns(false);
+ displayAlertStub.resolves(1);
+ sinon.stub(doc, 'cursorInField').resolves(doc.fields[0]);
+ sinon.stub(doc, 'canInsertField').resolves(false);
setAddEditItems(testItems[0]);
await execCommand('addEditCitation', docID);
@@ -538,17 +524,17 @@ describe("Zotero.Integration", function () {
var docID = this.test.fullTitle();
var doc = applications[docID].doc;
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.isNotOk(citation.properties.dontUpdate);
doc.fields[0].text = "modified";
// Return Yes
- displayAlertStub.returns(1);
+ displayAlertStub.resolves(1);
await execCommand('refresh', docID);
assert.isTrue(displayAlertStub.called);
assert.equal(doc.fields.length, 2);
assert.equal(doc.fields[0].text, "modified");
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.isOk(citation.properties.dontUpdate);
});
it('should reset citation text if "no" selected in refresh prompt', async function() {
@@ -556,18 +542,18 @@ describe("Zotero.Integration", function () {
var docID = this.test.fullTitle();
var doc = applications[docID].doc;
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.isNotOk(citation.properties.dontUpdate);
let origText = doc.fields[0].text;
doc.fields[0].text = "modified";
// Return No
- displayAlertStub.returns(0);
+ displayAlertStub.resolves(0);
await execCommand('refresh', docID);
assert.isTrue(displayAlertStub.called);
assert.equal(doc.fields.length, 2);
assert.equal(doc.fields[0].text, origText);
- var citation = (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
+ var citation = await (new Zotero.Integration.CitationField(doc.fields[0], doc.fields[0].code)).unserialize();
assert.isNotOk(citation.properties.dontUpdate);
});
});
@@ -673,11 +659,11 @@ describe("Zotero.Integration", function () {
var field = setTextSpy.firstCall.thisValue;
for (let i = 0; i < setTextSpy.callCount; i++) {
- assert.isTrue(field.equals(setTextSpy.getCall(i).thisValue));
+ assert.isTrue(yield field.equals(setTextSpy.getCall(i).thisValue));
}
for (let i = 0; i < setCodeSpy.callCount; i++) {
- assert.isTrue(field.equals(setCodeSpy.getCall(i).thisValue));
+ assert.isTrue(yield field.equals(setCodeSpy.getCall(i).thisValue));
}
setTextSpy.restore();
@@ -689,7 +675,7 @@ describe("Zotero.Integration", function () {
describe('#addEditBibliography', function() {
var docID = this.fullTitle();
beforeEach(function* () {
- initDoc(docID);
+ yield initDoc(docID);
yield execCommand('addEditCitation', docID);
});
@@ -699,7 +685,7 @@ describe("Zotero.Integration", function () {
assert.isFalse(displayDialogStub.called);
var biblPresent = false;
for (let i = applications[docID].doc.fields.length-1; i >= 0; i--) {
- let field = Zotero.Integration.Field.loadExisting(applications[docID].doc.fields[i]);
+ let field = yield Zotero.Integration.Field.loadExisting(applications[docID].doc.fields[i]);
if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) {
biblPresent = true;
break;