"use strict"; /* ***** BEGIN LICENSE BLOCK ***** Copyright © 2009 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 . ***** END LICENSE BLOCK ***** */ const RESELECT_KEY_URI = 1; const RESELECT_KEY_ITEM_KEY = 2; const RESELECT_KEY_ITEM_ID = 3; const DATA_VERSION = 3; // Specifies that citations should only be updated if changed const FORCE_CITATIONS_FALSE = 0; // Specifies that citations should only be updated if formattedText has changed from what is encoded // in the field code const FORCE_CITATIONS_REGENERATE = 1; // Specifies that citations should be reset regardless of whether formattedText has changed const FORCE_CITATIONS_RESET_TEXT = 2; // this is used only for update checking const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/AddonManager.jsm"); const INTEGRATION_MIN_VERSIONS = ["3.1.7.SOURCE", "3.5b2.SOURCE", "3.1.3.SOURCE"]; var _tmpFile = null; var _osascriptFile; // these need to be global because of GC var _updateTimer; // For Carbon and X11 var _carbon, ProcessSerialNumber, SetFrontProcessWithOptions; var _x11, _x11Display, _x11RootWindow, XClientMessageEvent, XFetchName, XFree, XQueryTree, XOpenDisplay, XCloseDisplay, XFlush, XDefaultRootWindow, XInternAtom, XSendEvent, XMapRaised, XGetWindowProperty, X11Atom, X11Bool, X11Display, X11Window, X11Status; this.currentWindow = false; this.sessions = {}; /** * Initializes the pipe used for integration on non-Windows platforms. */ this.init = function() { // We only use an integration pipe on OS X. // On Linux, we use the alternative communication method in the OOo plug-in // On Windows, we use a command line handler for integration. See // components/zotero-integration-service.js for this implementation. if(!Zotero.isMac) return; // Determine where to put the pipe // on OS X, first try /Users/Shared for those who can't put pipes in their home // directories var pipe = null; var sharedDir = Components.classes["@mozilla.org/file/local;1"]. createInstance(Components.interfaces.nsILocalFile); sharedDir.initWithPath("/Users/Shared"); if(sharedDir.exists() && sharedDir.isDirectory()) { var logname = Components.classes["@mozilla.org/process/environment;1"]. getService(Components.interfaces.nsIEnvironment). get("LOGNAME"); var sharedPipe = sharedDir.clone(); sharedPipe.append(".zoteroIntegrationPipe_"+logname); if(sharedPipe.exists()) { if(this.deletePipe(sharedPipe) && sharedDir.isWritable()) { pipe = sharedPipe; } } else if(sharedDir.isWritable()) { pipe = sharedPipe; } } if(!pipe) { // on other platforms, or as a fallback, use home directory pipe = Components.classes["@mozilla.org/file/directory_service;1"]. getService(Components.interfaces.nsIProperties). get("Home", Components.interfaces.nsIFile); pipe.append(".zoteroIntegrationPipe"); // destroy old pipe, if one exists if(!this.deletePipe(pipe)) return; } // try to initialize pipe try { this.initPipe(pipe); } catch(e) { Zotero.logError(e); } Zotero.Promise.delay(1000).then(_checkPluginVersions); } /** * Begin listening for integration commands on the given pipe * @param {String} pipe The path to the pipe */ this.initPipe = function(pipe) { Zotero.IPC.Pipe.initPipeListener(pipe, function(string) { if(string != "") { // exec command if possible var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/); if(parts) { var agent = parts[1].toString(); var cmd = parts[2].toString(); var document = parts[3] ? parts[3].toString() : null; Zotero.Integration.execCommand(agent, cmd, document); } else { Components.utils.reportError("Zotero: Invalid integration input received: "+string); } } }); } /** * Deletes a defunct pipe on OS X */ this.deletePipe = function(pipe) { try { if(pipe.exists()) { Zotero.IPC.safePipeWrite(pipe, "Zotero shutdown\n"); pipe.remove(false); } return true; } catch (e) { // if pipe can't be deleted, log an error Zotero.debug("Error removing old integration pipe "+pipe.path, 1); Zotero.logError(e); Components.utils.reportError( "Zotero word processor integration initialization failed. " + "See http://forums.zotero.org/discussion/12054/#Item_10 " + "for instructions on correcting this problem." ); // can attempt to delete on OS X try { var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe")); if(!deletePipe) return false; let escapedFifoFile = pipe.path.replace("'", "'\\''"); _executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); if(pipe.exists()) return false; } catch(e) { Zotero.logError(e); return false; } } } /** * Checks to see that plugin versions are up to date. * @return {Promise} Promise that is resolved with true if versions are up to date * or with false if they are not. */ var _checkPluginVersions = new function () { var integrationVersionsOK; return function _checkPluginVersions() { if(integrationVersionsOK) { if(integrationVersionsOK === true) { return Zotero.Promise.resolve(integrationVersionsOK); } else { return Zotero.Promise.reject(integrationVersionsOK); } } var deferred = Zotero.Promise.defer(); AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, function(addons) { for(var i in addons) { var addon = addons[i]; if(!addon || addon.userDisabled) continue; if(Services.vc.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) { deferred.reject(integrationVersionsOK = new Zotero.Exception.Alert( "integration.error.incompatibleVersion2", [Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]], "integration.error.title")); } } deferred.resolve(integrationVersionsOK = true); }); return deferred.promise; }; } /** * Executes an integration command, first checking to make sure that versions are compatible */ this.execCommand = new function() { var inProgress; return function execCommand(agent, command, docId) { var document; if(inProgress) { Zotero.Integration.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; } inProgress = true; // Check integration component versions _checkPluginVersions().then(function() { // Try to load the appropriate Zotero component; otherwise display an error try { var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); var application = Components.classes[componentClass] .getService(Components.interfaces.zoteroIntegrationApplication); } catch(e) { throw new Zotero.Exception.Alert("integration.error.notInstalled", [], "integration.error.title"); } // Try to execute the command; otherwise display an error in alert service or word processor // (depending on what is possible) document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); return Zotero.Promise.resolve((new Zotero.Integration.Document(application, document))[command]()); }).catch(function(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(document) { try { document.activate(); document.displayAlert(displayError, Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); } catch(e) { showErrorInFirefox = true; } } if(showErrorInFirefox) { Zotero.Integration.activate(); Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService) .alert(null, Zotero.getString("integration.error.title"), displayError); } } } finally { Zotero.logError(e); } } }) .finally(function() { if(document) { try { document.cleanup(); document.activate(); // Call complete function if one exists if(document.wrappedJSObject && document.wrappedJSObject.complete) { document.wrappedJSObject.complete(); } } catch(e) { Zotero.logError(e); } } if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { var oldWindow = Zotero.Integration.currentWindow; Zotero.Promise.delay(100).then(function() { oldWindow.close(); }); } inProgress = Zotero.Integration.currentWindow = false; }); }; }; /** * Activates Firefox */ this.activate = function(win) { if(Zotero.isMac) { const BUNDLE_IDS = { "Zotero":"org.zotero.zotero", "Firefox":"org.mozilla.firefox", "Aurora":"org.mozilla.aurora", "Nightly":"org.mozilla.nightly" }; if(win) { Components.utils.import("resource://gre/modules/ctypes.jsm"); win.focus(); if(!_carbon) { _carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon"); /* * struct ProcessSerialNumber { * unsigned long highLongOfPSN; * unsigned long lowLongOfPSN; * }; */ ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", [{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]); /* * OSStatus SetFrontProcessWithOptions ( * const ProcessSerialNumber *inProcess, * OptionBits inOptions * ); */ SetFrontProcessWithOptions = _carbon.declare("SetFrontProcessWithOptions", ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, ctypes.uint32_t); } var psn = new ProcessSerialNumber(); psn.highLongOfPSN = 0; psn.lowLongOfPSN = 2 // kCurrentProcess win.addEventListener("load", function() { var res = SetFrontProcessWithOptions( psn.address(), 1 // kSetFrontProcessFrontWindowOnly = (1 << 0) ); }, false); } else { _executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate'); } } else if(!Zotero.isWin && win) { Components.utils.import("resource://gre/modules/ctypes.jsm"); if(_x11 === false) return; if(!_x11) { try { _x11 = ctypes.open("libX11.so.6"); } catch(e) { try { var libName = ctypes.libraryName("X11"); } catch(e) { _x11 = false; Zotero.debug("Integration: Could not get libX11 name; not activating"); Zotero.logError(e); return; } try { _x11 = ctypes.open(libName); } catch(e) { _x11 = false; Zotero.debug("Integration: Could not open "+libName+"; not activating"); Zotero.logError(e); return; } } X11Atom = ctypes.unsigned_long; X11Bool = ctypes.int; X11Display = new ctypes.StructType("Display"); X11Window = ctypes.unsigned_long; X11Status = ctypes.int; /* * typedef struct { * int type; * unsigned long serial; / * # of last request processed by server * / * Bool send_event; / * true if this came from a SendEvent request * / * Display *display; / * Display the event was read from * / * Window window; * Atom message_type; * int format; * union { * char b[20]; * short s[10]; * long l[5]; * } data; * } XClientMessageEvent; */ XClientMessageEvent = new ctypes.StructType("XClientMessageEvent", [ {"type":ctypes.int}, {"serial":ctypes.unsigned_long}, {"send_event":X11Bool}, {"display":X11Display.ptr}, {"window":X11Window}, {"message_type":X11Atom}, {"format":ctypes.int}, {"l0":ctypes.long}, {"l1":ctypes.long}, {"l2":ctypes.long}, {"l3":ctypes.long}, {"l4":ctypes.long} ] ); /* * Status XFetchName( * Display* display, * Window w, * char** window_name_return * ); */ XFetchName = _x11.declare("XFetchName", ctypes.default_abi, X11Status, X11Display.ptr, X11Window, ctypes.char.ptr.ptr); /* * Status XQueryTree( * Display* display, * Window w, * Window* root_return, * Window* parent_return, * Window** children_return, * unsigned int* nchildren_return * ); */ XQueryTree = _x11.declare("XQueryTree", ctypes.default_abi, X11Status, X11Display.ptr, X11Window, X11Window.ptr, X11Window.ptr, X11Window.ptr.ptr, ctypes.unsigned_int.ptr); /* * int XFree( * void* data * ); */ XFree = _x11.declare("XFree", ctypes.default_abi, ctypes.int, ctypes.voidptr_t); /* * Display *XOpenDisplay( * _Xconst char* display_name * ); */ XOpenDisplay = _x11.declare("XOpenDisplay", ctypes.default_abi, X11Display.ptr, ctypes.char.ptr); /* * int XCloseDisplay( * Display* display * ); */ XCloseDisplay = _x11.declare("XCloseDisplay", ctypes.default_abi, ctypes.int, X11Display.ptr); /* * int XFlush( * Display* display * ); */ XFlush = _x11.declare("XFlush", ctypes.default_abi, ctypes.int, X11Display.ptr); /* * Window XDefaultRootWindow( * Display* display * ); */ XDefaultRootWindow = _x11.declare("XDefaultRootWindow", ctypes.default_abi, X11Window, X11Display.ptr); /* * Atom XInternAtom( * Display* display, * _Xconst char* atom_name, * Bool only_if_exists * ); */ XInternAtom = _x11.declare("XInternAtom", ctypes.default_abi, X11Atom, X11Display.ptr, ctypes.char.ptr, X11Bool); /* * Status XSendEvent( * Display* display, * Window w, * Bool propagate, * long event_mask, * XEvent* event_send * ); */ XSendEvent = _x11.declare("XSendEvent", ctypes.default_abi, X11Status, X11Display.ptr, X11Window, X11Bool, ctypes.long, XClientMessageEvent.ptr); /* * int XMapRaised( * Display* display, * Window w * ); */ XMapRaised = _x11.declare("XMapRaised", ctypes.default_abi, ctypes.int, X11Display.ptr, X11Window); /* * extern int XGetWindowProperty( * Display* display, * Window w, * Atom property, * long long_offset, * long long_length, * Bool delete, * Atom req_type, * Atom* actual_type_return, * int* actual_format_return, * unsigned long* nitems_return, * unsigned long* bytes_after_return, * unsigned char** prop_return * ); */ XGetWindowProperty = _x11.declare("XGetWindowProperty", ctypes.default_abi, ctypes.int, X11Display.ptr, X11Window, X11Atom, ctypes.long, ctypes.long, X11Bool, X11Atom, X11Atom.ptr, ctypes.int.ptr, ctypes.unsigned_long.ptr, ctypes.unsigned_long.ptr, ctypes.char.ptr.ptr); _x11Display = XOpenDisplay(null); if(!_x11Display) { Zotero.debug("Integration: Could not open display; not activating"); _x11 = false; return; } Zotero.addShutdownListener(function() { XCloseDisplay(_x11Display); }); _x11RootWindow = XDefaultRootWindow(_x11Display); if(!_x11RootWindow) { Zotero.debug("Integration: Could not get root window; not activating"); _x11 = false; return; } } win.addEventListener("load", function() { var intervalID; intervalID = win.setInterval(function() { _X11BringToForeground(win, intervalID); }, 50); }, false); } } /** * Get a property from an X11 window */ function _X11GetProperty(win, propertyName, propertyType) { Components.utils.import("resource://gre/modules/ctypes.jsm"); var returnType = new X11Atom(), returnFormat = new ctypes.int(), nItemsReturned = new ctypes.unsigned_long(), nBytesAfterReturn = new ctypes.unsigned_long(), data = new ctypes.char.ptr(); if(!XGetWindowProperty(_x11Display, win, XInternAtom(_x11Display, propertyName, 0), 0, 1024, 0, propertyType, returnType.address(), returnFormat.address(), nItemsReturned.address(), nBytesAfterReturn.address(), data.address())) { var nElements = ctypes.cast(nItemsReturned, ctypes.unsigned_int).value; if(nElements) return [data, nElements]; } return null; } /** * Bring a window to the foreground by interfacing directly with X11 */ function _X11BringToForeground(win, intervalID) { var windowTitle = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIBaseWindow).title; var x11Window = _X11FindWindow(_x11RootWindow, windowTitle); if(!x11Window) return; win.clearInterval(intervalID); var event = new XClientMessageEvent(); event.type = 33; /* ClientMessage*/ event.serial = 0; event.send_event = 1; event.message_type = XInternAtom(_x11Display, "_NET_ACTIVE_WINDOW", 0); event.display = _x11Display; event.window = x11Window; event.format = 32; event.l0 = 2; var mask = 1<<20 /* SubstructureRedirectMask */ | 1<<19 /* SubstructureNotifyMask */; if(XSendEvent(_x11Display, _x11RootWindow, 0, mask, event.address())) { XMapRaised(_x11Display, x11Window); XFlush(_x11Display); Zotero.debug("Integration: Activated successfully"); } else { Zotero.debug("Integration: An error occurred activating the window"); } } /** * Find an X11 window given a name */ function _X11FindWindow(w, searchName) { Components.utils.import("resource://gre/modules/ctypes.jsm"); var res = _X11GetProperty(w, "_NET_CLIENT_LIST", 33 /** XA_WINDOW **/) || _X11GetProperty(w, "_WIN_CLIENT_LIST", 6 /** XA_CARDINAL **/); if(!res) return false; var nClients = res[1], clientList = ctypes.cast(res[0], X11Window.array(nClients).ptr).contents, foundName = new ctypes.char.ptr(); for(var i=0; i DATA_VERSION) { return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.newerDocumentVersion", [data.zoteroVersion, Zotero.version], "integration.error.title")); } if(data.prefs.fieldType !== this._app.primaryFieldType && data.prefs.fieldType !== this._app.secondaryFieldType) { return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", [], "integration.error.title")); } if(Zotero.Integration.sessions[data.sessionID]) { this._session = Zotero.Integration.sessions[data.sessionID]; } else { this._session = this._createNewSession(data); try { this._session.setData(data); } catch(e) { // make sure style is defined if(e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, this._app.secondaryFieldType).then(function(status) { me._doc.setDocumentData(me._session.data.serializeXML()); me._session.reload = true; return me._session; }); } else { return Zotero.Promise.reject(e); } } this._doc.setDocumentData(this._session.data.serializeXML()); this._session.reload = true; } return Zotero.Promise.resolve(this._session); } }; /** * Adds a citation to the current document. * @return {Promise} */ Zotero.Integration.Document.prototype.addCitation = function() { var me = this; return this._getSession(false, false).then(function() { return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); }); } /** * Edits the citation at the cursor position. * @return {Promise} */ Zotero.Integration.Document.prototype.editCitation = function() { var me = this; return this._getSession(true, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); if(!field) { throw new Zotero.Exception.Alert("integration.error.notInCitation", [], "integration.error.title"); } return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(field); }); } /** * Edits the citation at the cursor position if one exists, or else adds a new one. * @return {Promise} */ Zotero.Integration.Document.prototype.addEditCitation = function() { var me = this; return this._getSession(false, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(field); }); } /** * Adds a bibliography to the current document. * @return {Promise} */ Zotero.Integration.Document.prototype.addBibliography = function() { var me = this; return this._getSession(true, false).then(function() { // Make sure we can have a bibliography if(!me._session.data.style.hasBibliography) { throw new Zotero.Exception.Alert("integration.error.noBibliography", [], "integration.error.title"); } var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); return fieldGetter.addField().then(function(field) { field.setCode("BIBL"); return fieldGetter.updateSession().then(function() { return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); }); }); } /** * Edits bibliography metadata. * @return {Promise} */ Zotero.Integration.Document.prototype.editBibliography = function() { // Make sure we have a bibliography var me = this, fieldGetter; return this._getSession(true, false).then(function() { fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); return fieldGetter.get(); }).then(function(fields) { var haveBibliography = false; for(var i=fields.length-1; i>=0; i--) { var code = fields[i].getCode(); var [type, content] = fieldGetter.getCodeTypeAndContent(code); if(type == INTEGRATION_TYPE_BIBLIOGRAPHY) { haveBibliography = true; break; } } if(!haveBibliography) { throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", [], "integration.error.title"); } return fieldGetter.updateSession(); }).then(function() { return me._session.editBibliography(me._doc); }).then(function() { return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); } /** * Updates the citation data for all citations and bibliography entries. * @return {Promise} */ Zotero.Integration.Document.prototype.refresh = function() { var me = this; return this._getSession(true, false).then(function() { // Send request, forcing update of citations and bibliography var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); return fieldGetter.updateSession().then(function() { return fieldGetter.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); }); }); } /** * Deletes field codes. * @return {Promise} */ Zotero.Integration.Document.prototype.removeCodes = function() { var me = this; return this._getSession(true, false).then(function() { var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); return fieldGetter.get() }).then(function(fields) { var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); if(result) { for(var i=fields.length-1; i>=0; i--) { fields[i].removeCode(); } } }); } /** * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) * @return {Promise} */ Zotero.Integration.Document.prototype.setDocPrefs = function() { var me = this, fieldGetter, oldData; return this._getSession(false, true).then(function(haveSession) { fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); var setDocPrefs = me._session.setDocPrefs.bind(me._session, me._doc, me._app.primaryFieldType, me._app.secondaryFieldType); if(!haveSession) { // This is a brand new document; don't try to get fields return setDocPrefs(); } else if(me._session.reload) { // Always reload before setDocPrefs so we can permit/deny unchecking storeReferences as // appropriate return fieldGetter.updateSession().then(setDocPrefs); } else { // Can get fields while dialog is open return Zotero.Promise.all([ fieldGetter.get(), setDocPrefs() ]).spread(function (fields, setDocPrefs) { // Only return value from setDocPrefs return setDocPrefs; }); } }).then(function(aOldData) { // After setDocPrefs call oldData = aOldData; // Write document data to document me._doc.setDocumentData(me._session.data.serializeXML()); // If oldData is null, then there was no document data, so we don't need to update // fields if(!oldData) return false; return fieldGetter.get(); }).then(function(fields) { if(!fields || !fields.length) return; // If there are fields, we will have to convert some things; get a list of what // we need to deal with var convertBibliographies = oldData === true || oldData.prefs.fieldType != me._session.data.prefs.fieldType; var convertItems = convertBibliographies || oldData.prefs.noteType != me._session.data.prefs.noteType; var fieldsToConvert = new Array(); var fieldNoteTypes = new Array(); for(var i=0, n=fields.length; i {}) .then(function() { me._session.updateIndices = {}; me._session.updateItemIDs = {}; me._session.citationText = {}; me._session.bibliographyHasChanged = false; delete me._session.reload; }); } else { return; } }); } /** * Keep processing fields until all have been processed */ Zotero.Integration.Fields.prototype._processFields = function(i) { if(!i) i = 0; for(var n = this._fields.length; i {}).then(function() { return Zotero.Promise.each( me._updateDocument( forceCitations, forceBibliography, ignoreCitationChanges ), () => {} ); }); } /** * Helper function to update bibliographys and fields within a document * @param {Boolean} forceCitations Whether to regenerate all citations * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been * modified since they were created, instead of showing a warning */ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, forceBibliography, ignoreCitationChanges) { if(this.progressCallback) { var nFieldUpdates = Object.keys(this._session.updateIndices).length; if(this._session.bibliographyHasChanged || forceBibliography) { nFieldUpdates += this._bibliographyFields.length*5; } } var nUpdated=0; for(var i in this._session.updateIndices) { if(this.progressCallback && nUpdated % 10 == 0) { try { this.progressCallback(75+(nUpdated/nFieldUpdates)*25); } catch(e) { Zotero.logError(e); } yield; } var citation = this._session.citationsByIndex[i]; var field = this._fields[i]; // If there is no citation, we're deleting it, or we shouldn't update it, ignore // it if(!citation || citation.properties.delete) continue; var isRich = false; if(!citation.properties.dontUpdate) { var formattedCitation = citation.properties.custom ? citation.properties.custom : this._session.citationText[i]; if(formattedCitation.indexOf("\\") !== -1) { // need to set text as RTF formattedCitation = "{\\rtf "+formattedCitation+"}" isRich = true; } if(forceCitations === FORCE_CITATIONS_RESET_TEXT || citation.properties.formattedCitation !== formattedCitation) { // Check if citation has been manually modified if(!ignoreCitationChanges && citation.properties.plainCitation) { var plainCitation = field.getText(); if(plainCitation !== citation.properties.plainCitation) { // Citation manually modified; ask user if they want to save changes Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" + "Original: " + citation.properties.plainCitation + "\n" + "Current: " + plainCitation ); field.select(); var result = this._doc.displayAlert( Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO); if(result) { citation.properties.dontUpdate = true; } } } if(!citation.properties.dontUpdate) { field.setText(formattedCitation, isRich); citation.properties.formattedCitation = formattedCitation; citation.properties.plainCitation = field.getText(); } } } var fieldCode = this._session.getCitationField(citation); if(fieldCode != citation.properties.field) { field.setCode( (this._session.data.prefs.storeReferences ? "ITEM CSL_CITATION" : "ITEM") +" "+fieldCode); if(this._session.data.prefs.fieldType === "ReferenceMark" && isRich && !citation.properties.dontUpdate) { // For ReferenceMarks with formatting, we need to set the text again, because // setting the field code removes formatting from the mark. I don't like this. field.setText(formattedCitation, isRich); } } nUpdated++; } // update bibliographies if(this._bibliographyFields.length // if bibliography exists && (this._session.bibliographyHasChanged // and bibliography changed || forceBibliography)) { // or if we should generate regardless of // changes var bibliographyFields = this._bibliographyFields; if(forceBibliography || this._session.bibliographyDataHasChanged) { var bibliographyData = this._session.getBibliographyData(); for (let field of bibliographyFields) { field.setCode("BIBL "+bibliographyData +(this._session.data.prefs.storeReferences ? " CSL_BIBLIOGRAPHY" : "")); } } // get bibliography and format as RTF var bib = this._session.getBibliography(); var bibliographyText = ""; if(bib) { bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+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, bibStyle.lineSpacing, bibStyle.entrySpacing, bibStyle.tabStops, bibStyle.tabStops.length); // set bibliographyStyleHasBeenSet parameter to prevent further changes this._session.data.style.bibliographyStyleHasBeenSet = true; this._doc.setDocumentData(this._session.data.serializeXML()); } } // set bibliography text for (let field of bibliographyFields) { if(this.progressCallback) { try { this.progressCallback(75+(nUpdated/nFieldUpdates)*25); } catch(e) { Zotero.logError(e); } yield; } if(bibliographyText) { field.setText(bibliographyText, true); } else { field.setText("{Bibliography}", false); } nUpdated += 5; } } // Do these operations in reverse in case plug-ins care about order for(var i=this._session.citationsByIndex.length-1; i>=0; i--) { if(this._session.citationsByIndex[i] && this._session.citationsByIndex[i].properties.delete) { this._fields[i].delete(); } } var removeCodeFields = Object.keys(this._removeCodeFields).sort(); for(var i=(removeCodeFields.length-1); i>=0; i--) { this._fields[removeCodeFields[i]].removeCode(); } } /** * Brings up the addCitationDialog, prepopulated if a citation is provided */ Zotero.Integration.Fields.prototype.addEditCitation = function(field) { var newField, citation, fieldIndex, session = this._session; // if there's already a citation, make sure we have item IDs in addition to keys if(field) { try { var code = field.getCode(); } catch(e) {} if(code) { var [type, content] = this.getCodeTypeAndContent(code); if(type != INTEGRATION_TYPE_ITEM) { throw new Zotero.Exception.Alert("integration.error.notInCitation"); } try { citation = session.unserializeCitation(content); } catch(e) {} if(citation) { try { session.lookupItems(citation); } catch(e) { if(e instanceof Zotero.Integration.MissingItemException) { citation.citationItems = []; } else { throw e; } } if(citation.properties.dontUpdate || (citation.properties.plainCitation && field.getText() !== citation.properties.plainCitation)) { this._doc.activate(); Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" + "citation.properties.dontUpdate: " + citation.properties.dontUpdate + "\n" + "Original: " + citation.properties.plainCitation + "\n" + "Current: " + field.getText() ); if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"), Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) { throw new Zotero.Exception.UserCancelled("editing citation"); } } // make sure it's going to get updated delete citation.properties["formattedCitation"]; delete citation.properties["plainCitation"]; delete citation.properties["dontUpdate"]; } } } else { newField = true; field = this.addField(true); } var me = this; return Zotero.Promise.resolve(field).then(function(field) { if(!citation) { field.setCode("TEMP"); citation = {"citationItems":[], "properties":{}}; } var io = new Zotero.Integration.CitationEditInterface(citation, field, me, session); if(Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { Zotero.Integration.displayDialog(me._doc, 'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', io); } else { var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') ? 'popup' : 'alwaysRaised')+',resizable=false'; Zotero.Integration.displayDialog(me._doc, 'chrome://zotero/content/integration/quickFormat.xul', mode, io); } if(newField) { return io.promise.catch(function(e) { // Try to delete new field on failure try { field.delete(); } catch(e) {} throw e; }); } else { return io.promise; } }); } /** * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js */ Zotero.Integration.CitationEditInterface = function(citation, field, fieldGetter, session) { this.citation = citation; this._field = field; this._fieldGetter = fieldGetter; this._session = session; this._sessionUpdateResolveErrors = false; this._sessionUpdateDeferreds = []; // Needed to make this work across boundaries this.wrappedJSObject = this; // Determine whether citation is sortable in current style this.sortable = session.style.opt.sort_citations; // Citeproc-js style object for use of third-party extension this.style = session.style; // Start getting citation data this._acceptDeferred = Zotero.Promise.defer(); this._fieldIndexPromise = fieldGetter.get().then(function(fields) { for(var i=0, n=fields.length; i { return citationsByItemID[itemID] && citationsByItemID[itemID].length // Exclude the present item && (citationsByItemID[itemID].length > 1 || citationsByItemID[itemID][0].properties.zoteroIndex !== this._fieldIndex); }); // Sort all previously cited items at top, and all items cited later at bottom var fieldIndex = this._fieldIndex; ids.sort(function(a, b) { var indexA = citationsByItemID[a][0].properties.zoteroIndex, indexB = citationsByItemID[b][0].properties.zoteroIndex; if(indexA >= fieldIndex){ if(indexB < fieldIndex) return 1; return indexA - indexB; } if(indexB > fieldIndex) return -1; return indexB - indexA; }); return Zotero.Cite.getItem(ids); } } /** * Keeps track of all session-specific variables */ Zotero.Integration.Session = function(doc) { // holds items not in document that should be in bibliography this.uncitedItems = {}; this.omittedItems = {}; this.embeddedItems = {}; this.embeddedZoteroItems = {}; this.embeddedZoteroItemsByURI = {}; this.customBibliographyText = {}; this.reselectedItems = {}; this.resetRequest(doc); } /** * Resets per-request variables in the CitationSet */ Zotero.Integration.Session.prototype.resetRequest = function(doc) { this.uriMap = new Zotero.Integration.URIMap(this); this.regenerateAll = false; this.bibliographyHasChanged = false; this.bibliographyDataHasChanged = false; this.updateItemIDs = {}; this.updateIndices = {}; this.newIndices = {}; this.oldCitationIDs = this.citeprocCitationIDs; this.citationsByItemID = {}; this.citationsByIndex = []; this.documentCitationIDs = {}; this.citeprocCitationIDs = {}; this.citationText = {}; this.doc = doc; } /** * Changes the Session style and data * @param data {Zotero.Integration.DocumentData} * @param resetStyle {Boolean} Whether to force the style to be reset * regardless of whether it has changed. This is desirable if the * automaticJournalAbbreviations or locale has changed. */ Zotero.Integration.Session.prototype.setData = function(data, resetStyle) { var oldStyle = (this.data && this.data.style ? this.data.style : false); this.data = data; if(data.style.styleID && (!oldStyle || oldStyle.styleID != data.style.styleID || resetStyle)) { this.styleID = data.style.styleID; try { 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.styleClass = getStyle.class; this.dateModified = new Object(); } catch(e) { Zotero.logError(e); data.style.styleID = undefined; throw new Zotero.Exception.Alert("integration.error.invalidStyle"); } return true; } else if(oldStyle) { data.style = oldStyle; } return false; } /** * Displays a dialog to set document preferences * @return {Promise} A promise resolved with old document data, if there was any or null, * if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was * cancelled. */ Zotero.Integration.Session.prototype.setDocPrefs = function(doc, primaryFieldType, secondaryFieldType) { var io = new function() { this.wrappedJSObject = this; }; if(this.data) { io.style = this.data.style.styleID; io.locale = this.data.style.locale; io.useEndnotes = this.data.prefs.noteType == 0 ? 0 : this.data.prefs.noteType-1; io.fieldType = this.data.prefs.fieldType; io.primaryFieldType = primaryFieldType; io.secondaryFieldType = secondaryFieldType; io.storeReferences = this.data.prefs.storeReferences; io.automaticJournalAbbreviations = this.data.prefs.automaticJournalAbbreviations; io.requireStoreReferences = !Zotero.Utilities.isEmpty(this.embeddedItems); } var me = this; return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io) .then(function() { if(!io.style) { throw new Zotero.Exception.UserCancelled("document preferences window"); } // set data var oldData = me.data; var data = new Zotero.Integration.DocumentData(); data.sessionID = oldData.sessionID; data.style.styleID = io.style; data.style.locale = io.locale; data.prefs.fieldType = io.fieldType; data.prefs.storeReferences = io.storeReferences; data.prefs.automaticJournalAbbreviations = io.automaticJournalAbbreviations; var forceStyleReset = oldData && ( oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations || oldData.style.locale != io.locale ); me.setData(data, forceStyleReset); // need to do this after setting the data so that we know if it's a note style me.data.prefs.noteType = me.style && me.styleClass == "note" ? io.useEndnotes+1 : 0; if(!oldData || oldData.style.styleID != data.style.styleID || oldData.prefs.noteType != data.prefs.noteType || oldData.prefs.fieldType != data.prefs.fieldType || oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations) { // This will cause us to regenerate all citations me.oldCitationIDs = {}; } return oldData || null; }); } /** * Reselects an item to replace a deleted item * @param exception {Zotero.Integration.MissingItemException} */ Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) { var io = new function() { this.wrappedJSObject = this; }, me = this; io.addBorder = Zotero.isWin; io.singleSelection = true; return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul', 'resizable', io).then(function() { if(io.dataOut && io.dataOut.length) { var itemID = io.dataOut[0]; // add reselected item IDs to hash, so they can be used for each(var reselectKey in exception.reselectKeys) { me.reselectedItems[reselectKey] = itemID; } // add old URIs to map, so that they will be included if(exception.reselectKeyType == RESELECT_KEY_URI) { me.uriMap.add(itemID, exception.reselectKeys.concat(me.uriMap.getURIsForItemID(itemID))); } // flag for update me.updateItemIDs[itemID] = true; } }); } /** * Generates a field from a citation object */ Zotero.Integration.Session.prototype.getCitationField = function(citation) { const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", "suffix"]; var addSchema = false; var type; var field = []; field.push('"citationID":'+uneval(citation.citationID)); var properties = JSON.stringify(citation.properties, saveProperties); if(properties != "{}") { field.push('"properties":'+properties); } var m = citation.citationItems.length; var citationItems = new Array(m); for(var j=0; j parseInt(i))); } /** * Refreshes updateIndices variable to include fields for modified items */ Zotero.Integration.Session.prototype.updateUpdateIndices = function(regenerateAll) { if(regenerateAll || this.regenerateAll) { // update all indices for(var i in this.citationsByIndex) { this.newIndices[i] = true; this.updateIndices[i] = true; } } else { // update only item IDs for(var i in this.updateItemIDs) { if(this.citationsByItemID[i] && this.citationsByItemID[i].length) { for(var j=0; j [this.uriMap.getURIsForItemID(id), this.customBibliographyText[id]]); if(bibliographyData.uncited || bibliographyData.custom) { return JSON.stringify(bibliographyData); } else { return ""; // nothing } } /** * Returns a preview, given a citation object (whose citationItems lack item * and position) */ Zotero.Integration.Session.prototype.previewCitation = function(citation) { var citationsPre, citationsPost, citationIndices; [citationsPre, citationsPost, citationIndices] = this._getPrePost(citation.properties.zoteroIndex); try { return this.style.previewCitationCluster(citation, citationsPre, citationsPost, "rtf"); } catch(e) { throw e; } } /** * Edits integration bibliography */ Zotero.Integration.Session.prototype.editBibliography = function(doc) { var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this); var io = new function() { this.wrappedJSObject = bibliographyEditor; } this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); } /** * @class Interface for bibliography editor to alter document bibliography * @constructor * Creates a new bibliography editor interface * @param session {Zotero.Integration.Session} */ Zotero.Integration.Session.BibliographyEditInterface = function(session) { this.session = session; this._changed = { "customBibliographyText":{}, "uncitedItems":{}, "omittedItems":{} } for(var list in this._changed) { for(var key in this.session[list]) { this._changed[list][key] = this.session[list][key]; } } this._update(); } /** * Updates stored bibliography */ Zotero.Integration.Session.BibliographyEditInterface.prototype._update = function() { this.session.updateUncitedItems(); this.session.style.setOutputFormat("rtf"); this.bibliography = this.session.style.makeBibliography(); Zotero.Cite.removeFromBibliography(this.bibliography, this.session.omittedItems); for(var i in this.bibliography[0].entry_ids) { if(this.bibliography[0].entry_ids[i].length != 1) continue; var itemID = this.bibliography[0].entry_ids[i][0]; if(this.session.customBibliographyText[itemID]) { this.bibliography[1][i] = this.session.customBibliographyText[itemID]; } } } /** * Reverts the text of an individual bibliography entry */ Zotero.Integration.Session.BibliographyEditInterface.prototype.revert = function(itemID) { delete this.session.customBibliographyText[itemID]; this._update(); } /** * Reverts bibliography to condition in which no edits have been made */ Zotero.Integration.Session.BibliographyEditInterface.prototype.revertAll = function() { for(var list in this._changed) { this.session[list] = {}; } this._update(); } /** * Reverts bibliography to condition before BibliographyEditInterface was opened * Does not run _update automatically, since this will usually only happen with a cancel request */ Zotero.Integration.Session.BibliographyEditInterface.prototype.cancel = function() { for(var list in this._changed) { this.session[list] = this._changed[list]; } this.session.updateUncitedItems(); } /** * Checks whether a given reference is cited within the main document text */ Zotero.Integration.Session.BibliographyEditInterface.prototype.isCited = function(item) { if(this.session.citationsByItemID[item]) return true; } /** * Checks whether an item ID is cited in the bibliography being edited */ Zotero.Integration.Session.BibliographyEditInterface.prototype.isEdited = function(itemID) { if(this.session.customBibliographyText[itemID]) return true; return false; } /** * Checks whether any citations in the bibliography have been edited */ Zotero.Integration.Session.BibliographyEditInterface.prototype.isAnyEdited = function() { for(var list in this._changed) { for(var a in this.session[list]) { return true; } } return false; } /** * Adds an item to the bibliography */ Zotero.Integration.Session.BibliographyEditInterface.prototype.add = function(itemID) { if(this.session.omittedItems[itemID]) { delete this.session.omittedItems[itemID]; } else { this.session.uncitedItems[itemID] = true; } this._update(); } /** * Removes an item from the bibliography being edited */ Zotero.Integration.Session.BibliographyEditInterface.prototype.remove = function(itemID) { if(this.session.uncitedItems[itemID]) { delete this.session.uncitedItems[itemID]; } else { this.session.omittedItems[itemID] = true; } this._update(); } /** * Sets custom bibliography text for a given item */ Zotero.Integration.Session.BibliographyEditInterface.prototype.setCustomText = function(itemID, text) { this.session.customBibliographyText[itemID] = text; this._update(); } /** * A class for parsing and passing around document-specific data */ Zotero.Integration.DocumentData = function(string) { this.style = {}; this.prefs = {}; this.sessionID = null; if(string) { this.unserialize(string); } } /** * Serializes document-specific data as XML */ Zotero.Integration.DocumentData.prototype.serializeXML = function() { var prefs = ""; for(var pref in this.prefs) { prefs += ''; } return ''+ ''+ '