www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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:
Mchrome/content/zotero/bibliography.js | 46+++++++++++++++++++++++++++-------------------
Mchrome/content/zotero/integration/integrationDocPrefs.xul | 2+-
Achrome/content/zotero/xpcom/connector/httpIntegrationClient.js | 194+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/connector/server_connector.js | 1275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/connector/server_connectorIntegration.js | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/integration.js | 773++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mchrome/content/zotero/xpcom/server.js | 1+
Dchrome/content/zotero/xpcom/server_connector.js | 1277-------------------------------------------------------------------------------
Mcomponents/zotero-service.js | 15+++------------
Mtest/tests/integrationTest.js | 100++++++++++++++++++++++++++++++++++---------------------------------------------
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;