diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 1ff15220e..b5ee9af4b 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -106,15 +106,11 @@ var Zotero_File_Interface_Bibliography = new function() { styleChanged(selectIndex); } if(document.getElementById("formatUsing")) { - if(_io.useBookmarks && _io.useBookmarks == 1) document.getElementById("formatUsing").selectedIndex = 1; - if(_io.openOffice) { - var formatOption = "referenceMarks"; - } else { - var formatOption = "fields"; - } + if(_io.fieldType == "Bookmarks") document.getElementById("formatUsing").selectedIndex = 1; + Zotero.safeDebug(_io) + 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-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"); } @@ -170,7 +166,7 @@ var Zotero_File_Interface_Bibliography = new function() { // ONLY FOR integrationDocPrefs.xul: collect displayAs if(document.getElementById("displayAs")) { _io.useEndnotes = document.getElementById("displayAs").selectedIndex; - _io.useBookmarks = document.getElementById("formatUsing").selectedIndex; + _io.fieldType = (document.getElementById("formatUsing").selectedIndex == 0 ? _io.primaryFieldType : _io.secondaryFieldType); } // save style (this happens only for "Export Bibliography," or Word diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 55d9b4f0c..4d0771a79 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -1,545 +1,747 @@ /* ***** BEGIN LICENSE BLOCK ***** - - Copyright (c) 2006 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://chnm.gmu.edu - - Licensed under the Educational Community License, Version 1.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.opensource.org/licenses/ecl1.php - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + + Copyright (c) 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . ***** END LICENSE BLOCK ***** */ -const API_VERSION = 2; -const COMPAT_API_VERSION = 6; +const RESELECT_KEY_URI = 1; +const RESELECT_KEY_ITEM_KEY = 2; +const RESELECT_KEY_ITEM_ID = 3; Zotero.Integration = new function() { - var _contentLengthRe = /[\r\n]Content-Length: *([0-9]+)/i; - var _XMLRe = /<\?[^>]+\?>/; - var _onlineObserverRegistered; + var _fifoFile, _osascriptFile; this.sessions = {}; - var ns = "http://www.zotero.org/namespaces/SOAP"; - this.ns = new Namespace(ns); - - this.init = init; - this.handleHeader = handleHeader; - this.handleEnvelope = handleEnvelope; - this.__defineGetter__("usePopup", function () { return Zotero.isWin && !Zotero.Prefs.get("integration.realWindow"); }); - /* - * initializes a very rudimentary web server used for SOAP RPC + /** + * Initializes the pipe used for integration on non-Windows platforms. */ - function init() { - this.env = new Namespace("http://schemas.xmlsoap.org/soap/envelope/"); - - if (Zotero.Utilities.HTTP.browserIsOffline()) { - Zotero.debug('Browser is offline -- not initializing integration HTTP server'); - _registerOnlineObserver() - return; - } - - // start listening on socket - var serv = Components.classes["@mozilla.org/network/server-socket;1"] - .createInstance(Components.interfaces.nsIServerSocket); - try { - // bind to a random port on loopback only - serv.init(Zotero.Prefs.get('integration.port'), true, -1); - serv.asyncListen(Zotero.Integration.SocketListener); + this.init = function() { + if(!Zotero.isWin) { + // create a new file representing the pipe + _fifoFile = Components.classes["@mozilla.org/file/directory_service;1"]. + getService(Components.interfaces.nsIProperties). + get("Home", Components.interfaces.nsIFile); + _fifoFile.append(".zoteroIntegrationPipe"); - Zotero.debug("Integration HTTP server listening on 127.0.0.1:"+serv.port); - } catch(e) { - Zotero.debug("Not initializing integration HTTP server"); - } - - _registerOnlineObserver() - } - - /* - * handles an HTTP request - */ - function handleHeader(header) { - // get first line of request (all we care about for now) - var method = header.substr(0, header.indexOf(" ")); - - if(!method) { - return _generateResponse("400 Bad Request"); - } - - if(method != "POST") { - return _generateResponse("501 Method Not Implemented"); - } else { - // parse content length - var m = _contentLengthRe.exec(header); - if(!m) { - return _generateResponse("400 Bad Request"); - } else { - return parseInt(m[1]); - } - } - } - - /* - * handles a SOAP envelope - */ - function handleEnvelope(envelope) { - Zotero.debug("Integration: SOAP Request\n"+envelope); - envelope = envelope.replace(_XMLRe, ""); - var env = this.env; - - var xml = new XML(envelope); - var request = xml.env::Body.children()[0]; - if(request.namespace() != this.ns) { - Zotero.debug("Integration: SOAP method not supported: invalid namespace"); - } else if(!xml.env::Header.children().length()) { - // old style SOAP request - var name = request.localName(); - if(Zotero.Integration.SOAP_Compat[name]) { - if(request.input.length()) { - // split apart passed parameters (same colon-escaped format - // as we pass) - var input = request.input.toString(); - var vars = new Array(); - vars[0] = ""; - var i = 0; + // destroy old pipe, if one exists + if(_fifoFile.exists()) _fifoFile.remove(false); + + // make a new pipe + var mkfifo = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + mkfifo.initWithPath("/usr/bin/mkfifo"); + if(!mkfifo.exists()) mkfifo.initWithPath("/bin/mkfifo"); + if(!mkfifo.exists()) mkfifo.initWithPath("/usr/local/bin/mkfifo"); + + if(mkfifo.exists()) { + var main = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread; + var background = Components.classes["@mozilla.org/thread-manager;1"].getService().newThread(0); + + var me = this; + function mainThread(agent, cmd) { + this.agent = agent; + this.cmd = cmd; + } + mainThread.prototype.run = function() { + me.execCommand(this.agent, this.cmd); + } + + function fifoThread() {} + fifoThread.prototype.run = function() { + var proc = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + proc.init(mkfifo); + proc.run(true, [_fifoFile.path], 1); - var lastIndex = 0; - var colonIndex = input.indexOf(":", lastIndex); - while(colonIndex != -1) { - if(input[colonIndex+1] == ":") { // escaped - vars[i] += input.substring(lastIndex, colonIndex+1); - lastIndex = colonIndex+2; - } else { // not escaped - vars[i] += input.substring(lastIndex, colonIndex); - i++; - vars[i] = ""; - lastIndex = colonIndex+1; - } - colonIndex = input.indexOf(":", lastIndex); + if(!_fifoFile.exists()) Zotero.debug("Could not initialize Zotero integration pipe"); + + var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + var line = {}; + while(true) { + fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream); + fifoStream.init(_fifoFile, -1, 0, 0); + fifoStream.QueryInterface(Components.interfaces.nsILineInputStream); + fifoStream.readLine(line); + fifoStream.close(); + + var spaceIndex = line.value.indexOf(" "); + var agent = line.value.substr(0, spaceIndex); + var cmd = line.value.substr(spaceIndex+1); + if(agent == "Zotero" && cmd == "shutdown") return; + main.dispatch(new mainThread(agent, cmd), background.DISPATCH_NORMAL); } - vars[i] += input.substr(lastIndex); - } else { - var vars = null; } - // execute request - var output = Zotero.Integration.SOAP_Compat[name](vars); - - // ugh: we can't use real SOAP, since AppleScript VBA can't pass - // objects, so implode arrays - if(!output) { - output = ""; + fifoThread.prototype.QueryInterface = mainThread.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsIRunnable) || + iid.equals(Components.interfaces.nsISupports)) return this; + throw Components.results.NS_ERROR_NO_INTERFACE; } - if(typeof(output) == "object") { - for(var i in output) { - if(typeof(output[i]) == "string") { - output[i] = output[i].replace(/:/g, "::"); - } - } - output = output.join(":"); - } - - // create envelope - var responseEnvelope = - - - {output} - - - ; - - var response = '\n'+responseEnvelope.toXMLString(); - Zotero.debug("Integration: SOAP Response\n"+response); - - // return OK - return _generateResponse("200 OK", 'text/xml; charset="UTF-8"', - response); + background.dispatch(new fifoThread(), background.DISPATCH_NORMAL); + + var observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver({ + observe: me.destroy + }, "quit-application", false); } else { - Zotero.debug("Integration: SOAP method not supported"); + Zotero.debug("mkfifo not found -- not initializing integration pipe"); } - } else { - // execute request - request = new Zotero.Integration.Request(xml); - return _generateResponse(request.status+" "+request.statusText, - 'text/xml; charset="UTF-8"', request.responseText); } + + // initialize SOAP server just to throw version errors + Zotero.Integration.Compat.init(); } - /* - * generates the response to an HTTP request + /** + * Executes an integration command. */ - function _generateResponse(status, contentType, body) { - var response = "HTTP/1.0 "+status+"\r\n"; - - if(body) { - if(contentType) { - response += "Content-Type: "+contentType+"\r\n"; - } - response += "\r\n"+body; - } else { - response += "Content-Length: 0\r\n\r\n" - } - - return response; - } - - - function _registerOnlineObserver() { - if (_onlineObserverRegistered) { - return; - } - - // Observer to enable the integration when we go online - var observer = { - observe: function(subject, topic, data) { - if (data == 'online') { - Zotero.Integration.init(); - } - } - }; - - var observerService = - Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - observerService.addObserver(observer, "network:offline-status-changed", false); - - _onlineObserverRegistered = true; - } -} - -Zotero.Integration.SocketListener = new function() { - this.onSocketAccepted = onSocketAccepted; - this.onStopListening = onStopListening; - - /* - * called when a socket is opened - */ - function onSocketAccepted(socket, transport) { - // get an input stream - var iStream = transport.openInputStream(0, 0, 0); - var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0); - - var dataListener = new Zotero.Integration.DataListener(iStream, oStream); - var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"] - .createInstance(Components.interfaces.nsIInputStreamPump); - pump.init(iStream, -1, -1, 0, 0, false); - pump.asyncRead(dataListener, null); - } - - function onStopListening(serverSocket, status) { - Zotero.debug("Integration HTTP server going offline"); - } -} - -/* - * handles the actual acquisition of data - */ -Zotero.Integration.DataListener = function(iStream, oStream) { - this.header = ""; - this.headerFinished = false; - - this.body = ""; - this.bodyLength = 0; - - this.iStream = iStream; - this.oStream = oStream; - this.sStream = Components.classes["@mozilla.org/scriptableinputstream;1"] - .createInstance(Components.interfaces.nsIScriptableInputStream); - this.sStream.init(iStream); - - this.foundReturn = false; -} - -/* - * called when a request begins (although the request should have begun before - * the DataListener was generated) - */ -Zotero.Integration.DataListener.prototype.onStartRequest = function(request, context) {} - -/* - * called when a request stops - */ -Zotero.Integration.DataListener.prototype.onStopRequest = function(request, context, status) { - this.iStream.close(); - this.oStream.close(); -} - -/* - * called when new data is available - */ -Zotero.Integration.DataListener.prototype.onDataAvailable = function(request, context, - inputStream, offset, count) { - var readData = this.sStream.read(count); - - if(this.headerFinished) { // reading body - this.body += readData; - // check to see if data is done - this._bodyData(); - } else { // reading header - // see if there's a magic double return - var lineBreakIndex = readData.indexOf("\r\n\r\n"); - if(lineBreakIndex != -1) { - if(lineBreakIndex != 0) { - this.header += readData.substr(0, lineBreakIndex+4); - this.body = readData.substr(lineBreakIndex+4); - } - - this._headerFinished(); - return; - } - var lineBreakIndex = readData.indexOf("\n\n"); - if(lineBreakIndex != -1) { - if(lineBreakIndex != 0) { - this.header += readData.substr(0, lineBreakIndex+2); - this.body = readData.substr(lineBreakIndex+2); - } - - this._headerFinished(); - return; - } - if(this.header && this.header[this.header.length-1] == "\n" && - (readData[0] == "\n" || readData[0] == "\r")) { - if(readData.length > 1 && readData[1] == "\n") { - this.header += readData.substr(0, 2); - this.body = readData.substr(2); - } else { - this.header += readData[0]; - this.body = readData.substr(1); - } - - this._headerFinished(); - return; - } - this.header += readData; - } -} - -/* - * processes an HTTP header and decides what to do - */ -Zotero.Integration.DataListener.prototype._headerFinished = function() { - this.headerFinished = true; - var output = Zotero.Integration.handleHeader(this.header); - - if(typeof(output) == "number") { - this.bodyLength = output; - // check to see if data is done - this._bodyData(); - } else { - this._requestFinished(output); - } -} - -/* - * checks to see if Content-Length bytes of body have been read and, if they - * have, processes the body - */ -Zotero.Integration.DataListener.prototype._bodyData = function() { - if(this.body.length >= this.bodyLength) { - // convert to UTF-8 - var dataStream = Components.classes["@mozilla.org/io/string-input-stream;1"] - .createInstance(Components.interfaces.nsIStringInputStream); - dataStream.setData(this.body, this.bodyLength); - - var utf8Stream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Components.interfaces.nsIConverterInputStream); - utf8Stream.init(dataStream, "UTF-8", 4096, "?"); - - this.body = ""; - var string = {}; - while(utf8Stream.readString(this.bodyLength, string)) { - this.body += string.value; - } - - // handle envelope - var output = Zotero.Integration.handleEnvelope(this.body); - this._requestFinished(output); - } -} - -/* - * returns HTTP data from a request - */ -Zotero.Integration.DataListener.prototype._requestFinished = function(response) { - // close input stream - this.iStream.close(); - - // open UTF-8 converter for output stream - var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] - .createInstance(Components.interfaces.nsIConverterOutputStream); - - // write - try { - intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0)); - - // write response - intlStream.writeString(response); - } finally { - intlStream.close(); - } -} - -Zotero.Integration.Request = function(xml) { - var env = Zotero.Integration.env; - this.header = xml.env::Header; - this.body = xml.env::Body; - - this.responseXML = - - - - - default xml namespace = Zotero.Integration.ns; with({}); - this.responseHeader = this.responseXML.env::Header; - this.responseBody = this.responseXML.env::Body; - - this.needPrefs = this.body.setDocPrefs.length(); - - try { - this.initializeSession(); - if(this.needPrefs) { - this.setDocPrefs(); - } - if(this.body.reselectItem.length()) { - this.reselectItem(); - } else { - // if no more reselections, clear the reselectItem map - this._session.reselectItem = new Object(); - } - if(this.body.updateCitations.length() || this.body.updateBibliography.length()) { - this.processCitations(); - } - - this.status = 200; - this.statusText = "OK"; - } catch(e) { - Zotero.debug(e); - Components.utils.reportError(e); - - // Get a code for this error - var code = (e.name ? e.name : "GenericError"); - var text = e.toString(); + this.execCommand = function execCommand(agent, command) { + var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; + Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command); + var application = Components.classes[componentClass] + .getService(Components.interfaces.zoteroIntegrationApplication); + var integration = new Zotero.Integration.Document(application); try { - var text = Zotero.getString("integration.error."+e, Zotero.version); - code = e; - } catch(e) {} - - this.responseXML = - - - - XML-ENV:Sender - z:{code} - - - - {text} - - - - - this.status = 500; - this.statusText = "Internal Server Error"; + integration[command](); + } catch(e) { + integration._doc.displayAlert(Zotero.getString("integration.error.generic"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + throw e; + } finally { + integration.cleanup(); + } } - // Zap chars that we don't want in our output - this.responseText = this.responseXML.toXMLString().replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); - Zotero.debug("Integration: SOAP Response\n"+this.responseText); + /** + * Destroys the integration pipe. + */ + this.destroy = function() { + // send shutdown message to fifo thread + var oStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. + getService(Components.interfaces.nsIFileOutputStream); + oStream.init(_fifoFile, 0x02 | 0x10, 0, 0); + var cmd = "Zotero shutdown\n"; + oStream.write(cmd, cmd.length); + oStream.close(); + _fifoFile.remove(false); + } + + /** + * Activates Firefox + */ + this.activate = function() { + if(Zotero.isMac) { + if(_osascriptFile === undefined) { + _osascriptFile = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + _osascriptFile.initWithPath("/usr/bin/osascript"); + if(!_osascriptFile.exists()) _osascriptFile = false; + } + + if(_osascriptFile) { + var proc = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + proc.init(_osascriptFile); + proc.run(false, ['-e', 'tell application "Firefox" to activate'], 2); + } + } + } } /** - * Gets session data to associate with a request - **/ -Zotero.Integration.Request.prototype.initializeSession = function() { - default xml namespace = Zotero.Integration.ns; with({}); - - if(this.header.client.@api != API_VERSION) { - throw "incompatibleVersion"; - } - - var styleID = this.header.style.@id.toString(); - this._sessionID = this.header.session.@id.toString(); - if(this._sessionID === "" || !Zotero.Integration.sessions[this._sessionID]) { - this._sessionID = Zotero.randomString(); - this._session = Zotero.Integration.sessions[this._sessionID] = new Zotero.Integration.Session(); - - var preferences = {}; - for each(var pref in this.header.prefs.pref) { - preferences[pref.@name] = pref.@value.toString(); + * An exception thrown when a document contains an item that no longer exists in the current document. + * + * @param reselectKeys {Array} Keys representing the missing item + * @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants) + * @param citationIndex {Integer} The index of the missing item within the citation cluster + * @param citationLength {Integer} The number of items cited in this citation cluster + */ +Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) { + this.reselectKeys = reselectKeys; + this.reselectKeyType = reselectKeyType; + this.citationIndex = citationIndex; + this.citationLength = citationLength; +} +Zotero.Integration.MissingItemException.prototype.name = "MissingItemException"; +Zotero.Integration.MissingItemException.prototype.message = "An item in this document is missing from your Zotero library."; +Zotero.Integration.MissingItemException.prototype.toString = function() { + return this.name; +} + + +// Field code for an item +const ITEM_CODE = "ITEM" +// Field code for a bibliography +const BIBLIOGRAPHY_CODE = "BIBL" +// Placeholder for an empty bibliography +const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}" + +/** + * + */ +Zotero.Integration.Document = function(app) { + this._app = app; + this._doc = app.getActiveDocument(); +} + +/** + * Creates a new session + * @param data {Zotero.Integration.DocumentData} Document data for new session + */ +Zotero.Integration.Document.prototype._createNewSession = function(data) { + data.sessionID = Zotero.randomString(); + var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session(); + session.setData(data); + return session; +} + +/** + * Gets preferences for a document + * @param require {Boolean} Whether an error should be thrown if no preferences exist (otherwise, + * the set doc prefs dialog is shown) + * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no + * preferences exist + */ +Zotero.Integration.Document.prototype._getSession = function(require, dontRunSetDocPrefs) { + var dataString = this._doc.getDocumentData(); + Zotero.debug(dataString); + if(!dataString) { + if(require) { + this._doc.displayAlert(Zotero.getString("integration.error.mustInsertCitation"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + } else { + // Set doc prefs if no data string yet + this._session = this._createNewSession(new Zotero.Integration.DocumentData()); + if(dontRunSetDocPrefs) return false; + + var ret = this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); + if(!ret) return false; + // save doc prefs in doc + this._doc.setDocumentData(this._session.data.serializeXML()); } - - this.needPrefs = this.needPrefs || !this._session.setStyle(styleID, preferences); } else { - this._session = Zotero.Integration.sessions[this._sessionID]; + var data = new Zotero.Integration.DocumentData(dataString); + if(Zotero.Integration.sessions[data.sessionID]) { + this._session = Zotero.Integration.sessions[data.sessionID]; + } else { + this._session = this._createNewSession(data); + + // make sure style is defined + if(!this._session.style) { + this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); + } + this._doc.setDocumentData(this._session.data.serializeXML()); + } } - this.responseHeader.appendChild(); + this._session.resetRequest(); + return true; +} + +/** + * Gets all fields for a document + * @param require {Boolean} Whether an error should be thrown if no fields exist + */ +Zotero.Integration.Document.prototype._getFields = function(require, onlyCheck) { + if(this._fields) return true; + if(!this._session && !this._getSession(require, true)) return false; + + var fields = this._doc.getFields(this._session.data.prefs['fieldType']); + this._fields = []; + while(fields.hasMoreElements()) { + this._fields.push(fields.getNext().QueryInterface(Components.interfaces.zoteroIntegrationField)); + } + + if(require && !this._fields.length) { + this._doc.displayAlert(Zotero.getString("integration.error.mustInsertCitation"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + return false; + } + + return true; +} + +/** + * Checks that it is appropriate to add fields to the current document at the current + * positon, then adds one. + */ +Zotero.Integration.Document.prototype._addField = function(note) { + // Get citation types if necessary + if(!this._doc.canInsertField(this._session.data.prefs['fieldType'])) { + this._doc.displayAlert(Zotero.getString("integration.error.cannotInsertHere"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK) + return false; + } + + var field = this._doc.cursorInField(this._session.data.prefs['fieldType']); + if(field) { + if(!this._doc.displayAlert(Zotero.getString("integration.replace"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL)) return false; + } + + if(!field) { + var field = this._doc.insertField(this._session.data.prefs['fieldType'], + (note ? this._session.data.prefs["noteType"] : 0)); + } + + return field; +} + +/** + * Loads existing citations and bibliographies out of a document, and creates or edits fields + */ +Zotero.Integration.Document.prototype._updateSession = function(editField) { + var deleteKeys = {}; + this._deleteFields = []; + this._removeCodeFields = []; + this._bibliographyFields = []; + var bibliographyData = ""; + + // first collect entire bibliography + this._getFields(); + var editFieldIndex = false; + for(var i in this._fields) { + var field = this._fields[i]; + + if(editField && field.equals(editField)) { + editFieldIndex = i; + } else { + var fieldCode = field.getCode(); + + if(fieldCode.substr(0, ITEM_CODE.length) == ITEM_CODE) { + try { + this._session.addCitation(i, fieldCode.substr(ITEM_CODE.length+1)); + } catch(e) { + if(e instanceof Zotero.Integration.MissingItemException) { + // First, check if we've already decided to remove field codes from these + var reselect = true; + for each(var reselectKey in e.reselectKeys) { + if(deleteKeys[reselectKey]) { + this._removeCodeFields.push(i); + reselect = false; + break; + } + } + + if(reselect) { + // Ask user what to do with this item + if(e.citationLength == 1) { + var msg = Zotero.getString("integration.missingItem.single"); + } else { + var msg = Zotero.getString("integration.missingItem.multiple", e.citationIndex.toString()); + } + msg += '\n\n'+Zotero.getString('integration.missingItem.description'); + field.select(); + var result = this._doc.displayAlert(msg, 1, 3); + if(result == 0) { // Cancel + throw "Integration update canceled by user"; + } else if(result == 1) { // No + for each(var reselectKey in e.reselectKeys) { + deleteKeys[reselectKey] = true; + } + this._removeCodeFields.push(i); + } else { // Yes + // Display reselect item dialog + Zotero.Integration.activate(); + this._session.reselectItem(e); + // Now try again + this._session.addCitation(i, fieldCode.substr(ITEM_CODE.length+1)); + this._doc.activate(); + } + } + } else { + throw e; + } + } + } else if(fieldCode.substr(0, BIBLIOGRAPHY_CODE.length) == BIBLIOGRAPHY_CODE) { + this._bibliographyFields.push(field); + if(!this._session.bibliographyData && !bibliographyData) { + bibliographyData = field.getCode().substr(BIBLIOGRAPHY_CODE.length+1); + } + } + } + } + + // load uncited items from bibliography + if(bibliographyData && !this._session.bibliographyData) { + this._session.loadBibliographyData(bibliographyData); + } + + this._session.updateItemSet(); + + // create new citation or edit existing citation + if(editFieldIndex) { + this._session.updateCitations(editFieldIndex-1); + var editFieldCode = editField.getCode().substr(ITEM_CODE.length+1); + var editCitation = editFieldCode ? this._session.unserializeCitation(editFieldCode, editFieldIndex) : null; + + Zotero.Integration.activate(); + var added = this._session.editCitation(editFieldIndex, editCitation); + this._doc.activate(); + + if(!added) { + if(editFieldCode) { // cancelled editing; just add as if nothing happened + this._session.addCitation(editFieldIndex, editCitation); + } else { // cancelled creation; delete the citation + this._session.deleteCitation(editFieldIndex); + } + } + } +} + +/** + * Updates bibliographies and fields within a document + */ +Zotero.Integration.Document.prototype._updateDocument = function(forceCitations, forceBibliography) { + // update bibliographies + var output = new Array(); + if(this._bibliographyFields.length // if blbliography exists + && (this._session.bibliographyHasChanged // and bibliography changed + || forceBibliography)) { // or if we should generate regardless of changes + if(this._session.bibliographyDataHasChanged) { + var bibliographyData = this._session.getBibliographyData(); + for each(var field in this._bibliographyFields) { + field.setCode(BIBLIOGRAPHY_CODE+" "+bibliographyData); + } + } + + var bibliographyText = this._session.getBibliography(); + for each(var field in this._bibliographyFields) { + field.setText(bibliographyText, true); + } + } + + // update citations + this._session.updateUpdateIndices(forceCitations); + for(var i in this._session.updateIndices) { + citation = this._session.citationsByIndex[i]; + if(!citation) continue; + + if(citation.properties["delete"]) { + // delete citation + this._deleteFields.push(i); + } else if(!this.haveMissing) { + var fieldCode = this._session.getCitationField(citation); + if(fieldCode != citation.properties.field) { + this._fields[citation.properties.index].setCode(ITEM_CODE+" "+fieldCode); + } + + if(citation.properties.custom) { + var citationText = citation.properties.custom; + // XML uses real RTF, rather than the format used for + // integration, so we have to escape things properly + citationText = citationText.replace(/[\x7F-\uFFFF]/g, + Zotero.Integration.Session._rtfEscapeFunction). + replace("\t", "\\tab ", "g"); + } else { + var citationText = this._session.style.formatCitation(citation, "RTF"); + } + + if(citationText.indexOf("\\") !== -1) { + // need to set text as RTF + this._fields[citation.properties.index].setText("{\\rtf "+citationText+"}", true); + } else { + // set text as plain + this._fields[citation.properties.index].setText(citationText, false); + } + } + } + + // do this operations in reverse in case plug-ins care about order + for(var i=(this._deleteFields.length-1); i>=0; i--) { + this._fields[this._deleteFields[i]].delete(); + } + for(var i=(this._removeCodeFields.length-1); i>=0; i--) { + this._fields[this._removeCodeFields[i]].removeCode(); + } +} + +/** + * Adds a citation to the current document. + */ +Zotero.Integration.Document.prototype.addCitation = function() { + if(!this._getSession()) return; + + var field = this._addField(true); + if(!field) return; + + this._updateSession(field); + this._updateDocument(); } /** - * Sets preferences - **/ -Zotero.Integration.Request.prototype.setDocPrefs = function() { - default xml namespace = Zotero.Integration.ns; with({}); + * Edits the citation at the cursor position. + */ +Zotero.Integration.Document.prototype.editCitation = function() { + if(!this._getSession(true)) return; + var field = this._doc.cursorInField(this._session.data.prefs['fieldType']) + if(!field) { + this._doc.displayAlert(Zotero.getString("integration.error.notInCitation"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + return; + } + + this._updateSession(field); + this._updateDocument(false, false); +} + +/** + * Adds a bibliography to the current document. + */ +Zotero.Integration.Document.prototype.addBibliography = function() { + if(!this._getSession(true)) return; + + // Make sure we can have a bibliography + if(!this._session.style.hasBibliography) { + this._doc.displayAlert(Zotero.getString("integration.error.noBibliography"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + return; + } + + // Make sure we have some citations + if(!this._getFields(true)) return; + + var field = this._addField(); + if(!field) return; + var bibliographyData = this._session.getBibliographyData(); + field.setCode(BIBLIOGRAPHY_CODE+" "+bibliographyData); + this._fields.push(field); + + this._updateSession(); + this._updateDocument(false, true); +} + +/** + * Edits bibliography metadata. + */ +Zotero.Integration.Document.prototype.editBibliography = function() { + // Make sure we have a bibliography + if(!this._getFields(true)) return false; + var haveBibliography = false; + for(var i=this._fields.length-1; i>=0; i++) { + if(this._fields[i].getCode().substr(0, BIBLIOGRAPHY_CODE.length) == BIBLIOGRAPHY_CODE) { + haveBibliography = true; + break; + } + } + + if(!haveBibliography) { + this._doc.displayAlert(Zotero.getString("integration.error.mustInsertBibliography"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + return; + } + + this._updateSession(); + Zotero.Integration.activate(); + this._session.editBibliography(); + this._doc.activate(); + this._updateDocument(false, true); +} + +/** + * Updates the citation data for all citations and bibliography entries. + */ +Zotero.Integration.Document.prototype.refresh = function() { + if(!this._getFields(true)) return false; + + // Send request, forcing update of citations and bibliography + this._updateSession(); + this._updateDocument(true, true); +} + +/** + * Deletes field codes. + */ +Zotero.Integration.Document.prototype.removeCodes = function() { + if(!this._getFields(true)) return false; + + var result = this._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL); + if(result) { + for(var i=this._fields.length-1; i>=0; i--) { + this._fields[i].removeCode(); + } + } +} + + +/** + * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) + */ +Zotero.Integration.Document.prototype.setDocPrefs = function() { + if(this._getSession(false, true)) this._getFields(); + var oldData = this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); + if(oldData) { + this._doc.setDocumentData(this._session.data.serializeXML()); + if(this._fields && this._fields.length) { + // 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 != this._session.data.prefs.fieldType; + var convertItems = convertBibliographies || oldData.prefs.noteType != this._session.data.prefs.noteType; + var fieldsToConvert = new Array(); + var fieldNoteTypes = new Array(); + for each(var field in this._fields) { + var fieldCode = field.getCode(); + + if(convertItems && fieldCode.substr(0, ITEM_CODE.length) == ITEM_CODE) { + fieldsToConvert.push(field); + fieldNoteTypes.push(this._session.data.prefs.noteType); + } else if(convertBibliographies && fieldCode.substr(0, BIBLIOGRAPHY_CODE.length) == BIBLIOGRAPHY_CODE) { + fieldsToConvert.push(field); + fieldNoteTypes.push(0); + } + } + + if(fieldsToConvert.length) { + // pass to conversion function + this._doc.convert(new Zotero.Integration.Document.JSEnumerator(fieldsToConvert), + this._session.data.prefs.fieldType, fieldNoteTypes, fieldNoteTypes.length); + + // clear fields so that they will get collected again before refresh + this._fields = undefined; + } + + // refresh contents + this.refresh(); + } + } +} + +/** + * Cleans up any changes made before returning, even if an error occurred + */ +Zotero.Integration.Document.prototype.cleanup = function() { + this._doc.cleanup() +} + +/** + * An exceedingly simple nsISimpleEnumerator implementation + */ +Zotero.Integration.Document.JSEnumerator = function(objArray) { + this.objArray = objArray; +} +Zotero.Integration.Document.JSEnumerator.prototype.hasMoreElements = function() { + return this.objArray.length; +} +Zotero.Integration.Document.JSEnumerator.prototype.getNext = function() { + return this.objArray.shift(); +} + +/** + * Keeps track of all session-specific variables + */ +Zotero.Integration.Session = function() { + // holds items not in document that should be in bibliography + this.uncitedItems = new Object(); + this.reselectedItems = new Object(); +} + +/** + * Changes the Session style and data + * @param data {Zotero.Integration.DocumentData} + */ +Zotero.Integration.Session.prototype.setData = function(data) { + var oldStyleID = (this.data && this.data.style.styleID ? this.data.style.styleID : false); + this.data = data; + if(data.style.styleID && oldStyleID != data.style.styleID) { + this.styleID = data.style.styleID; + try { + this.style = Zotero.Styles.get(data.style.styleID).csl; + this.dateModified = new Object(); + + this.itemSet = this.style.createItemSet(); + this.loadUncitedItems(); + } catch(e) { + Zotero.debug(e) + data.style.styleID = undefined; + return false; + } + + return true; + } + return false; +} + +/** + * Displays a dialog to set document preferences + */ +Zotero.Integration.Session.prototype.setDocPrefs = function(primaryFieldType, secondaryFieldType) { var io = new function() { this.wrappedJSObject = this; }; - io.openOffice = this.header.client.@agent == "OpenOffice"; - - var oldStyle = io.style = this._session.styleID; - io.useEndnotes = this._session.prefs.useEndnotes; - io.useBookmarks = this._session.prefs.fieldType; + if(this.data) { + io.style = this.data.style.styleID; + 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; + } Components.classes["@mozilla.org/embedcomp/window-watcher;1"] .getService(Components.interfaces.nsIWindowWatcher) .openWindow(null, 'chrome://zotero/content/integrationDocPrefs.xul', '', 'chrome,modal,centerscreen' + (Zotero.isWin ? ',popup' : ''), io, true); - if(!oldStyle || oldStyle != io.style - || io.useEndnotes != this._session.prefs.useEndnotes - || io.useBookmarks != this._session.prefs.fieldType) { - this._session.regenerateAll = this._session.bibliographyHasChanged = true; - - if(oldStyle != io.style) { - this._session.setStyle(io.style, this._session.prefs); - } - } - this._session.prefs.useEndnotes = io.useEndnotes; - this._session.prefs.fieldType = io.useBookmarks; + if(!io.style) return false; - this.responseHeader.appendChild(