www

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

commit 136c47be5f936a20f03f80784d989558a95c602f
parent 6da149b22c19781bba6436f5b4115cca71cd75cb
Author: Simon Kornblith <simon@simonster.com>
Date:   Thu, 20 Aug 2009 05:02:55 +0000

Integration megacommit, part 2: Zotero code

Closes #884, final period missing when a citation is first added in note styles
Closes #1298, issues with footnotes and citations in OOo
Closes #1069, Use async HTTP calls for integration requests
Closes #1027, User-customizable integration port number
Closes #698, Migration away from VBA
Closes #1085, Migrate VBA plug-in to new XML-based API
Closes #792, Auto-updating of OO plugins 


Diffstat:
Mchrome/content/zotero/bibliography.js | 12++++--------
Mchrome/content/zotero/xpcom/integration.js | 1719+++++++++++++++++++++++++++++++++++++------------------------------------------
Achrome/content/zotero/xpcom/integration_compat.js | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/locale/en-US/zotero/zotero.properties | 15++++++++++++++-
Acomponents/zotero-integration-service.js | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcomponents/zotero-service.js | 1+
Aidl/zoteroIntegration.idl | 151+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alicense_integration.txt | 674+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 2118 insertions(+), 932 deletions(-)

diff --git 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 @@ -1,869 +1,693 @@ /* ***** 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 <http://www.gnu.org/licenses/>. ***** 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; - - 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); - } - vars[i] += input.substr(lastIndex); - } else { - var vars = null; - } - - // execute request - var output = Zotero.Integration.SOAP_Compat[name](vars); + // 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); - // ugh: we can't use real SOAP, since AppleScript VBA can't pass - // objects, so implode arrays - if(!output) { - output = ""; + var me = this; + function mainThread(agent, cmd) { + this.agent = agent; + this.cmd = cmd; + } + mainThread.prototype.run = function() { + me.execCommand(this.agent, this.cmd); } - if(typeof(output) == "object") { - for(var i in output) { - if(typeof(output[i]) == "string") { - output[i] = output[i].replace(/:/g, "::"); - } + 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); + + 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); } - output = output.join(":"); } - // create envelope - var responseEnvelope = <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> - <SOAP-ENV:Body> - <m:{name}Response xmlns:m={this.ns}> - <output>{output}</output> - </m:{name}Response> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope>; - - var response = '<?xml version="1.0" encoding="UTF-8"?>\n'+responseEnvelope.toXMLString(); - Zotero.debug("Integration: SOAP Response\n"+response); + 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; + } - // 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" + 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 { + 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(); } - - 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 + /** + * Destroys the integration pipe. */ - 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); + 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); } - function onStopListening(serverSocket, status) { - Zotero.debug("Integration HTTP server going offline"); + /** + * 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); + } + } } } -/* - * handles the actual acquisition of data +/** + * 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.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; +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; } -/* - * called when a request begins (although the request should have begun before - * the DataListener was generated) + +// 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.DataListener.prototype.onStartRequest = function(request, context) {} +Zotero.Integration.Document = function(app) { + this._app = app; + this._doc = app.getActiveDocument(); +} -/* - * called when a request stops +/** + * Creates a new session + * @param data {Zotero.Integration.DocumentData} Document data for new session */ -Zotero.Integration.DataListener.prototype.onStopRequest = function(request, context, status) { - this.iStream.close(); - this.oStream.close(); +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; } -/* - * called when new data is available +/** + * 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.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); - } +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; - this._headerFinished(); - return; + 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()); } - 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); - } + } else { + 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); - 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); + // make sure style is defined + if(!this._session.style) { + this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); } - - this._headerFinished(); - return; + this._doc.setDocumentData(this._session.data.serializeXML()); } - this.header += readData; } + + this._session.resetRequest(); + return true; } -/* - * processes an HTTP header and decides what to do +/** + * Gets all fields for a document + * @param require {Boolean} Whether an error should be thrown if no fields exist */ -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); +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 to see if Content-Length bytes of body have been read and, if they - * have, processes the body +/** + * Checks that it is appropriate to add fields to the current document at the current + * positon, then adds one. */ -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); +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; } -/* - * returns HTTP data from a request +/** + * Loads existing citations and bibliographies out of a document, and creates or edits fields */ -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); +Zotero.Integration.Document.prototype._updateSession = function(editField) { + var deleteKeys = {}; + this._deleteFields = []; + this._removeCodeFields = []; + this._bibliographyFields = []; + var bibliographyData = ""; - // write - try { - intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0)); + // first collect entire bibliography + this._getFields(); + var editFieldIndex = false; + for(var i in this._fields) { + var field = this._fields[i]; - // write response - intlStream.writeString(response); - } finally { - intlStream.close(); + 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); + } + } + } } -} -Zotero.Integration.Request = function(xml) { - var env = Zotero.Integration.env; - this.header = xml.env::Header; - this.body = xml.env::Body; - - this.responseXML = <SOAP-ENV:Envelope xmlns={Zotero.Integration.ns} - xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> - <SOAP-ENV:Header/> - <SOAP-ENV:Body/> - </SOAP-ENV:Envelope> - - default xml namespace = Zotero.Integration.ns; with({}); - this.responseHeader = this.responseXML.env::Header; - this.responseBody = this.responseXML.env::Body; + // load uncited items from bibliography + if(bibliographyData && !this._session.bibliographyData) { + this._session.loadBibliographyData(bibliographyData); + } - this.needPrefs = this.body.setDocPrefs.length(); + this._session.updateItemSet(); - 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(); - try { - var text = Zotero.getString("integration.error."+e, Zotero.version); - code = e; - } catch(e) {} + // 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; - this.responseXML = <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> - <SOAP-ENV:Body> - <SOAP-ENV:Fault> - <SOAP-ENV:Code> - <SOAP-ENV:Value>XML-ENV:Sender</SOAP-ENV:Value> - <SOAP-ENV:Subcode>z:{code}</SOAP-ENV:Subcode> - </SOAP-ENV:Code> - </SOAP-ENV:Fault> - <SOAP-ENV:Reason> - <SOAP-ENV:Text>{text}</SOAP-ENV:Text> - </SOAP-ENV:Reason> - </SOAP-ENV:Body> - </SOAP-ENV:Envelope> + Zotero.Integration.activate(); + var added = this._session.editCitation(editFieldIndex, editCitation); + this._doc.activate(); - this.status = 500; - this.statusText = "Internal Server Error"; + 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); + } + } } - - // 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); } /** - * Gets session data to associate with a request - **/ -Zotero.Integration.Request.prototype.initializeSession = function() { - default xml namespace = Zotero.Integration.ns; with({}); + * 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); + } + } - if(this.header.client.@api != API_VERSION) { - throw "incompatibleVersion"; + var bibliographyText = this._session.getBibliography(); + for each(var field in this._bibliographyFields) { + field.setText(bibliographyText, true); + } } - 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(); + // update citations + this._session.updateUpdateIndices(forceCitations); + for(var i in this._session.updateIndices) { + citation = this._session.citationsByIndex[i]; + if(!citation) continue; - var preferences = {}; - for each(var pref in this.header.prefs.pref) { - preferences[pref.@name] = pref.@value.toString(); + 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); + } } - - this.needPrefs = this.needPrefs || !this._session.setStyle(styleID, preferences); - } else { - this._session = Zotero.Integration.sessions[this._sessionID]; } - this.responseHeader.appendChild(<session id={this._sessionID}/>); + // 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(); + } } - + /** - * Sets preferences - **/ -Zotero.Integration.Request.prototype.setDocPrefs = function() { - default xml namespace = Zotero.Integration.ns; with({}); - - var io = new function() { - this.wrappedJSObject = this; - }; + * Adds a citation to the current document. + */ +Zotero.Integration.Document.prototype.addCitation = function() { + if(!this._getSession()) return; - io.openOffice = this.header.client.@agent == "OpenOffice"; + var field = this._addField(true); + if(!field) return; - var oldStyle = io.style = this._session.styleID; - io.useEndnotes = this._session.prefs.useEndnotes; - io.useBookmarks = this._session.prefs.fieldType; + this._updateSession(field); + this._updateDocument(); +} - 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); - } +/** + * 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._session.prefs.useEndnotes = io.useEndnotes; - this._session.prefs.fieldType = io.useBookmarks; - - this.responseHeader.appendChild(<style - id={io.style} class={this._session.style.class} - hasBibliography={this._session.style.hasBibliography}/>); - this.responseHeader.appendChild(<prefs> - <pref name="useEndnotes" value={io.useEndnotes}/> - <pref name="fieldType" value={io.useBookmarks}/> - </prefs>); - this.responseBody.appendChild(<setDocPrefsResponse/>); + + this._updateSession(field); + this._updateDocument(false, false); } /** - * Reselects an item to replace a deleted item - **/ -Zotero.Integration.Request.prototype.reselectItem = function() { - default xml namespace = Zotero.Integration.ns; with({}); + * 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; + } - var io = new function() { - this.wrappedJSObject = this; - }; - io.addBorder = Zotero.isWin; - io.singleSelection = true; + // Make sure we have some citations + if(!this._getFields(true)) return; - Components.classes["@mozilla.org/embedcomp/window-watcher;1"] - .getService(Components.interfaces.nsIWindowWatcher) - .openWindow(null,'chrome://zotero/content/selectItemsDialog.xul', '', - 'chrome,modal,centerscreen,resizable=yes' + (Zotero.isWin ? ',popup' : ''), io, true); + var field = this._addField(); + if(!field) return; + var bibliographyData = this._session.getBibliographyData(); + field.setCode(BIBLIOGRAPHY_CODE+" "+bibliographyData); + this._fields.push(field); - if(io.dataOut && io.dataOut.length) { - var id = this.body.reselectItem.@id.toString(); - if(id) { - this._session.reselectItem[id] = io.dataOut[0]; - this._session.updateItemIDs[io.dataOut[0]] = true; - // add to other URIs, if other URIs exist - if(this._session.missingURIs[id]) { - this._session.uriMap.add(io.dataOut[0], this._session.missingURIs[id].concat(this._session.uriMap.getURIsForItemID(io.dataOut[0]))); - this._session.missingURIs[id] = undefined; - } - } - } + this._updateSession(); + this._updateDocument(false, true); } /** - * Updates citations - **/ -Zotero.Integration.Request.prototype.processCitations = function() { - default xml namespace = Zotero.Integration.ns; with({}); - - // get whether to edit bibliography or edit a citation - var editCitationIndex = this.body.updateCitations.@edit.toString(); - - // first collect entire bibliography - var editCitation = false; - for each(var citation in this.header.citations.citation) { - // trim spacing characters - var citationData = Zotero.Utilities.prototype.trim(citation.toString()); - if(citation.@index.toString() === editCitationIndex) { - if(!citation.@new.toString()) { // new citation - // save citation data - editCitation = this._session.unserializeCitation(citationData, citation.@index.toString()); - } - } else { - this._session.addCitation(citation.@index.toString(), citationData); + * 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(!this._session.haveMissing) { - // load bibliography data here - if(this.header.bibliography.length()) { - this._session.loadBibliographyData(Zotero.Utilities.prototype.trim(this.header.bibliography.toString())); - } - - this._session.updateItemSet(); + if(!haveBibliography) { + this._doc.displayAlert(Zotero.getString("integration.error.mustInsertBibliography"), + Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP, + Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK); + return; } - if(!this._session.haveMissing) { - // create new citation - if(editCitationIndex) { - this._session.updateCitations(editCitationIndex-1); - var added = this._session.editCitation(editCitationIndex, editCitation); - if(!added) { - if(editCitation) { - this._session.addCitation(editCitationIndex, editCitation); - } else { - this._session.deleteCitation(editCitationIndex); - } - } - this._session.updateItemSet(); - } - this._session.updateCitations(); - - // edit bibliography - if(this.body.updateBibliography.@edit.toString()) { - this._session.editBibliography(); - } - - // update - var output = new Array(); - if(this.body.updateBibliography.length() // if we want updated bib - && (this._session.bibliographyHasChanged // and bibliography changed - || this.body.updateBibliography.@force.toString())) { // or if we should generate regardless of changes - if(this._session.bibliographyDataHasChanged) { - this.responseBody.updateBibliographyResponse.code = this._session.getBibliographyData(); - } - this.responseBody.updateBibliographyResponse.text = this._session.getBibliography(true); - } - } - - // get citations - if(this.body.updateCitations.length()) { - this.responseBody.updateCitationsResponse.citations = this._session.getCitations(!!this.body.updateCitations.@force.toString() || this._session.regenerateAll, true); - } - - // reset citationSet - this._session.resetRequest(); + this._updateSession(); + Zotero.Integration.activate(); + this._session.editBibliography(); + this._doc.activate(); + this._updateDocument(false, true); } -Zotero.Integration.SOAP_Compat = new function() { - // SOAP methods - this.update = update; - this.restoreSession = restoreSession; - this.setDocPrefs = setDocPrefs; +/** + * Updates the citation data for all citations and bibliography entries. + */ +Zotero.Integration.Document.prototype.refresh = function() { + if(!this._getFields(true)) return false; - /* - * generates a new citation for a given item - * ACCEPTS: sessionID, bibliographyMode, citationMode, editCitationIndex(, fieldIndex, fieldName)+ - * RETURNS: bibliography, documentData(, fieldIndex, fieldRename, fieldContent)+ - */ - function update(vars) { - if(!Zotero.Integration.sessions[vars[0]]) return "ERROR:sessionExpired"; - - var session = Zotero.Integration.sessions[vars[0]]; - var bibliographyMode = vars[1]; - var citationMode = vars[2]; - - // get whether to edit bibliography or edit a citation - var editCitationIndex = false; - var editBibliography = false; - if(vars[3] == "B") { - editBibliography = true; - } else if(vars[3] != "!") { - editCitationIndex = vars[3]; + // 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(); } - - // first collect entire bibliography - var editCitation = false; - for(var i=4; i<vars.length; i+=2) { - if(vars[i+1] == "X") { // new citation has field name X - // only one new/edited field at a time; others get deleted - if(editCitationIndex === false) { - editCitationIndex = vars[i]; - } else { - session.deleteCitation(vars[i]); + } +} + + +/** + * 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); } - } else if(editCitationIndex !== false && vars[i] == editCitationIndex) { - // save citation data - editCitation = session.unserializeCitation(vars[i+1], vars[i]); - } else { - session.addCitation(vars[i], vars[i+1]); } - } - - session.updateItemSet(); - - if(editCitationIndex) { - session.updateCitations(editCitationIndex-1); - var added = session.editCitation(editCitationIndex, editCitation); - if(!added) { - if(editCitation) { - session.addCitation(editCitationIndex, editCitation); - } else { - session.deleteCitation(editCitationIndex); - } + + 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; } - session.updateItemSet(); - } - session.updateCitations(); - - if(editBibliography) { - session.editBibliography(); - } - - // update - var output = new Array(); - if((bibliographyMode == "updated" // if we want updated bib - && session.bibliographyHasChanged) // and bibliography changed - || bibliographyMode == "true") { // or if we should generate regardless of changes - var bibliography = session.getBibliography(); - if(!bibliography) bibliography = "!"; - output.push(bibliography); - } else { // otherwise, send no bibliography - output.push("!"); + // refresh contents + this.refresh(); } - - if(session.bibliographyDataHasChanged) { - var data = session.getBibliographyData(); - output.push(data !== "" ? data : "X"); - } else { - output.push("!"); - } - - // get citations - output = output.concat(session.getCitations(citationMode == "all")); - - // reset citationSet - session.resetRequest(); - - return output; } - - /* - * restores a session, given all citations - * ACCEPTS: version, documentData, styleID, use-endnotes, use-bookmarks(, fieldIndex, fieldName)+ - * RETURNS: sessionID - */ - function restoreSession(vars) { - if(!vars || !_checkVersion(vars[0])) { - return "ERROR:"+Zotero.getString("integration.error.incompatibleVersion", Zotero.version); - } +} - try { - Zotero.Styles.get(vars[2]); - } catch(e) { - return "ERROR:prefsNeedReset"; - } - - var sessionID = Zotero.randomString(); - var session = Zotero.Integration.sessions[sessionID] = new Zotero.Integration.Session(); - session.setStyle(vars[2], {useEndnotes:vars[3], fieldType:vars[4]}); - - var encounteredItem = new Object(); - var newField = new Object(); - var regenerate = new Object(); - - for(var i=5; i<vars.length; i+=2) { - session.addCitation(vars[i], vars[i+1]); - } - - session.updateItemSet(session.citationsByItemID); - if(vars[1] != "!") session.loadBibliographyData(vars[1]); - session.sortItemSet(); - session.resetRequest(); - - return [sessionID]; - } - - /* - * sets document preferences - * ACCEPTS: (sessionID | "!"), version - * RETURNS: version, sessionID, styleID, style-class, has-bibliography, use-endnotes, use-bookmarks - */ - function setDocPrefs(vars) { - if(!vars || !vars.length || !_checkVersion(vars[1])) { - return "ERROR:"+Zotero.getString("integration.error.incompatibleVersion", Zotero.version); - } - - var io = new function() { - this.wrappedJSObject = this; - } - - var version = vars[1].split("/"); - if(version[2].substr(0, 3) == "OOo") { - io.openOffice = true; - } - - var oldStyle = false; - if(vars[0] == "!") { - // no session ID; generate a new one - var sessionID = Zotero.randomString(); - var session = Zotero.Integration.sessions[sessionID] = new Zotero.Integration.Session(); - } else { - // session ID exists - var sessionID = vars[0]; - var session = Zotero.Integration.sessions[sessionID]; - if(!session) return "ERROR:sessionExpired"; - - oldStyle = io.style = session.styleID; - io.useEndnotes = session.prefs.useEndnotes; - io.useBookmarks = session.prefs.fieldType; - } - - Components.classes["@mozilla.org/embedcomp/window-watcher;1"] - .getService(Components.interfaces.nsIWindowWatcher) - .openWindow( - null, 'chrome://zotero/content/integrationDocPrefs.xul', '', - 'chrome,modal,centerscreen' - + (Zotero.Integration.usePopup ? ',popup' : ''), - io, true - ); - session.prefs.useEndnotes = io.useEndnotes; - session.prefs.fieldType = io.useBookmarks; - session.setStyle(io.style, session.prefs); - if(!oldStyle || oldStyle != io.style) { - session.regenerateAll = session.bibliographyHasChanged = true; - } - - return [sessionID, io.style, session.style.class, session.style.hasBibliography ? "1" : "0", io.useEndnotes, io.useBookmarks]; - } - - /* - * checks to see whether this version of the Integration API is compatible - * with the given version of the plug-in - */ - function _checkVersion(version) { - versionParts = version.split("/"); - Zotero.debug("Integration: client version "+version); - if(versionParts.length != 3 || versionParts[1] != COMPAT_API_VERSION) return false; - return true; - } +/** + * Cleans up any changes made before returning, even if an error occurred + */ +Zotero.Integration.Document.prototype.cleanup = function() { + this._doc.cleanup() } -/* - * keeps track of all session-specific variables +/** + * 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.prefs = new Object(); - this.reselectItem = new Object(); - - this.resetRequest(); + this.reselectedItems = new Object(); } -/* - * changes the Session style +/** + * Changes the Session style and data + * @param data {Zotero.Integration.DocumentData} */ -Zotero.Integration.Session.prototype.setStyle = function(styleID, prefs) { - this.prefs = prefs; - this.missingURIs = new Object(); - if(styleID) { - this.styleID = styleID; +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(styleID).csl; + 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) - this.styleID = undefined; + data.style.styleID = undefined; return false; } @@ -872,8 +696,81 @@ Zotero.Integration.Session.prototype.setStyle = function(styleID, prefs) { return false; } -/* - * resets per-request variables in the CitationSet +/** + * Displays a dialog to set document preferences + */ +Zotero.Integration.Session.prototype.setDocPrefs = function(primaryFieldType, secondaryFieldType) { + var io = new function() { + this.wrappedJSObject = this; + }; + + 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(!io.style) return false; + + // set data + var oldData = this.data; + var data = new Zotero.Integration.DocumentData(); + data.sessionID = oldData.sessionID; + data.style.styleID = io.style; + data.prefs.fieldType = io.fieldType; + this.setData(data); + // need to do this after setting the data so that we know if it's a note style + this.data.prefs.noteType = this.style && this.style.class == "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) { + this.regenerateAll = this.bibliographyHasChanged = true; + } + + return oldData ? oldData : true; +} + +/** + * Reselects an item to replace a deleted item + * @param exception {Zotero.Integration.MissingItemException} + */ +Zotero.Integration.Session.prototype.reselectItem = function(exception) { + var io = new function() { + this.wrappedJSObject = this; + }; + io.addBorder = Zotero.isWin; + io.singleSelection = true; + + Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher) + .openWindow(null,'chrome://zotero/content/selectItemsDialog.xul', '', + 'chrome,modal,centerscreen,resizable=yes' + (Zotero.isWin ? ',popup' : ''), io, true); + + 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) { + this.reselectedItems[reselectKey] = itemID; + } + // add old URIs to map, so that they will be included + if(exception.reselectKeyType == RESELECT_KEY_URI) { + this.uriMap.add(itemID, exception.reselectKeys.concat(this.uriMap.getURIsForItemID(itemID))); + } + // flag for update + this.updateItemIDs[itemID] = true; + } +} + +/** + * Resets per-request variables in the CitationSet */ Zotero.Integration.Session.prototype.resetRequest = function() { this.citationsByItemID = new Object(); @@ -888,8 +785,8 @@ Zotero.Integration.Session.prototype.resetRequest = function() { this.updateIndices = new Object(); } -/* - * generates a field from a citation object +/** + * Generates a field from a citation object */ Zotero.Integration.Session._acceptableTypes = ["string", "boolean", "number"]; Zotero.Integration.Session._saveProperties = ["custom", "sort"]; @@ -925,8 +822,8 @@ Zotero.Integration.Session.prototype.getCitationField = function(citation) { return "{"+field.substr(1)+"}"; } -/* - * adds a citation based on a serialized Word field +/** + * Adds a citation based on a serialized Word field */ Zotero.Integration._oldCitationLocatorMap = { p:Zotero.CSL.LOCATOR_PAGES, @@ -934,8 +831,8 @@ Zotero.Integration._oldCitationLocatorMap = { l:Zotero.CSL.LOCATOR_LINE }; -/* - * gets a Zotero.CSL.Citation object given a field name +/** + * Gets a Zotero.CSL.Citation object given a field name */ Zotero.Integration.Session.prototype.addCitation = function(index, arg) { var index = parseInt(index, 10); @@ -948,46 +845,40 @@ Zotero.Integration.Session.prototype.addCitation = function(index, arg) { var citation = arg; } - var completed = this.completeCitation(citation); + this.completeCitation(citation); // add to citationsByItemID and citationsByIndex - if(completed) { - for(var i=0; i<citation.citationItems.length; i++) { - var citationItem = citation.citationItems[i]; - if(!this.citationsByItemID[citationItem.itemID]) { - this.citationsByItemID[citationItem.itemID] = [citation]; + for(var i=0; i<citation.citationItems.length; i++) { + var citationItem = citation.citationItems[i]; + if(!this.citationsByItemID[citationItem.itemID]) { + this.citationsByItemID[citationItem.itemID] = [citation]; + } else { + var byItemID = this.citationsByItemID[citationItem.itemID]; + if(byItemID[byItemID.length-1].properties.index < index) { + // if index is greater than the last index, add to end + byItemID.push(citation); } else { - var byItemID = this.citationsByItemID[citationItem.itemID]; - if(byItemID[byItemID.length-1].properties.index < index) { - // if index is greater than the last index, add to end - byItemID.push(citation); - } else { - // otherwise, splice in at appropriate location - for(var j=0; byItemID[j].properties.index < index && j<byItemID.length-1; j++) {} - byItemID.splice(j, 0, citation); - } + // otherwise, splice in at appropriate location + for(var j=0; byItemID[j].properties.index < index && j<byItemID.length-1; j++) {} + byItemID.splice(j, 0, citation); } } - } else { - this.updateIndices[index] = true; - this.haveMissing = true; } citation.properties.index = index; this.citationsByIndex[index] = citation; } -/* - * adds items to a citation whose citationItems contain only item IDs +/** + * Adds items to a citation whose citationItems contain only item IDs */ Zotero.Integration.Session.prototype.completeCitation = function(object) { // replace item IDs with real items - var missing = []; - var missingItems = []; + var err; for(var i=0; i<object.citationItems.length; i++) { var citationItem = object.citationItems[i]; - // get Zotero item (dealing with reselected items along the way) + // get Zotero item var zoteroItem = false; if(citationItem.uri) { var needUpdate = false; @@ -1004,32 +895,30 @@ Zotero.Integration.Session.prototype.completeCitation = function(object) { } // if no item, check if it was already reselected and otherwise handle as a missing item - if(!zoteroItem) { + if(!zoteroItem) { if(citationItem.uri) { - var reselectKey = citationItem.uri[0]; + var reselectKeys = citationItem.uri; + var reselectKeyType = RESELECT_KEY_URI; } else if(citationItem.key) { - var reselectKey = citationItem.key; + var reselectKeys = citationItem.key; + var reselectKeyType = RESELECT_KEY_ITEM_KEY; } else { - var reselectKey = citationItem.itemID; + var reselectKeys = citationItem.itemID; + var reselectKeyType = RESELECT_KEY_ITEM_ID; } - if(this.reselectItem[reselectKey]) { - zoteroItem = Zotero.Items.get(this.reselectItem[reselectKey]); - - delete citationItem.uri; - delete citationItem.key; - citationItem.itemID = zoteroItem.itemID; - } else { - // item does not exist - missing.push(i); - missingItems.push(reselectKey); - - // save URIs so that we can append to existing list on reselect - if(citationItem.uri) { - this.missingURIs[citationItem.uri[0]] = citationItem.uri; + // look to see if item has already been reselected + for each(var reselectKey in reselectKeys) { + if(this.reselectedItems[reselectKey]) { + zoteroItem = Zotero.Items.get(this.reselectedItems[reselectKey]); + break; } - - continue; + } + + // if not already reselected, throw a MissingItemException + if(!zoteroItem) { + throw(new Zotero.Integration.MissingItemException( + reselectKeys, reselectKeyType, i, object.citationItems.length)); } } @@ -1047,17 +936,11 @@ Zotero.Integration.Session.prototype.completeCitation = function(object) { if(!citationItem.itemID) citationItem.itemID = item.id; } - if(missing.length) { - object.properties.missing = missing; - object.properties.missingItems = missingItems; - return false; - } - - return true; + return null; } -/* - * unserializes a JSON citation into a citation object (sans items) +/** + * Unserializes a JSON citation into a citation object (sans items) */ Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) { if(arg[0] == "{") { // JSON field @@ -1133,16 +1016,16 @@ Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) return citation; } -/* - * marks a citation for removal +/** + * <arks a citation for removal */ Zotero.Integration.Session.prototype.deleteCitation = function(index) { this.citationsByIndex[index] = {properties:{"delete":true}}; this.updateIndices[index] = true; } -/* - * returns a preview, given a citation object (whose citationItems lack item +/** + * Returns a preview, given a citation object (whose citationItems lack item * and position) and an index */ Zotero.Integration.Session.prototype.previewCitation = function(citation) { @@ -1172,8 +1055,8 @@ Zotero.Integration.Session.prototype.previewCitation = function(citation) { } -/* - * brings up the addCitationDialog, prepopulated if a citation is provided +/** + * Brings up the addCitationDialog, prepopulated if a citation is provided */ Zotero.Integration.Session.prototype.editCitation = function(index, citation) { var me = this; @@ -1212,7 +1095,7 @@ Zotero.Integration.Session.prototype.editCitation = function(index, citation) { + (Zotero.Integration.usePopup ? ',popup' : ''), io ); - + if(citation && !io.citation.citationItems.length) { io.citation = citation; } @@ -1228,8 +1111,8 @@ Zotero.Integration.Session.prototype.editCitation = function(index, citation) { return !!io.citation.citationItems.length; } -/* - * sets position attribute on a citation +/** + * Sets position attribute on a citation */ Zotero.Integration.Session.prototype.getCitationPositions = function(citation, update) { for(var previousIndex = citation.properties.index-1; @@ -1276,8 +1159,8 @@ Zotero.Integration.Session.prototype.getCitationPositions = function(citation, u } } -/* - * marks citations for update, where necessary +/** + * Marks citations for update, where necessary */ Zotero.Integration.Session.prototype.updateCitations = function(toIndex) { if(!toIndex) toIndex = this.citationsByIndex.length-1; @@ -1290,9 +1173,8 @@ Zotero.Integration.Session.prototype.updateCitations = function(toIndex) { } } -/* - * updates the ItemSet, adding and deleting bibliography items as appropriate, - * then re-sorting +/** + * Updates the ItemSet, adding and deleting bibliography items as appropriate, then re-sorting */ Zotero.Integration.Session.prototype.updateItemSet = function() { var deleteItems = []; @@ -1330,24 +1212,11 @@ Zotero.Integration.Session.prototype.updateItemSet = function() { this.bibliographyHasChanged = true; } - // add missing attribute to citations of missing items - if(missingItems.length) { - for each(var i in missingItems) { - if(this.citationsByItemID[i].length) { - for(var j=0; j<this.citationsByItemID[i].length; j++) { - this.updateIndices[this.citationsByItemID[i][j].properties.index] = true; - this.completeCitation(this.citationsByItemID[i][j]); - } - } - } - this.haveMissing = true; - } else { - this.sortItemSet(); - } + this.sortItemSet(); } -/* - * sorts the ItemSet (what did you think it did?) +/** + * Sorts the ItemSet */ Zotero.Integration.Session.prototype.sortItemSet = function() { // save first index @@ -1367,8 +1236,8 @@ Zotero.Integration.Session.prototype.sortItemSet = function() { } } -/* - * edits integration bibliography +/** + * Edits integration bibliography */ Zotero.Integration.Session.prototype.editBibliography = function() { var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this); @@ -1387,29 +1256,24 @@ Zotero.Integration.Session.prototype.editBibliography = function() { ); } -/* - * gets integration bibliography +/** + * Gets integration bibliography */ -Zotero.Integration.Session.prototype.getBibliography = function(useXML) { - // get preview citation - if(useXML) { - // use real RTF in XML incarnation, but chop off the first \n - var text = this.style.formatBibliography(this.itemSet, "RTF") - var nlIndex = text.indexOf("\n"); - if(nlIndex !== -1) { - return "{\\rtf "+text.substr(text.indexOf("\n")); - } else { - return ""; - } +Zotero.Integration.Session.prototype.getBibliography = function() { + // use real RTF, but chop off the first \n + var text = this.style.formatBibliography(this.itemSet, "RTF") + var nlIndex = text.indexOf("\n"); + if(nlIndex !== -1) { + return "{\\rtf "+text.substr(text.indexOf("\n")); } else { - return this.style.formatBibliography(this.itemSet, "Integration"); + return ""; } } -/* - * gets citations in need of update +/** + * Refreshes updateIndices variable to include fields for modified items */ -Zotero.Integration.Session.prototype.getCitations = function(regenerateAll, useXML) { +Zotero.Integration.Session.prototype.updateUpdateIndices = function(regenerateAll) { if(regenerateAll || this.regenerateAll) { // update all indices for(var i=0; i<this.citationsByIndex.length; i++) { @@ -1425,87 +1289,14 @@ Zotero.Integration.Session.prototype.getCitations = function(regenerateAll, useX } } } - - var output = (useXML ? <citations/> : []); - var citation; - - for(var i in this.updateIndices) { - citation = this.citationsByIndex[i]; - - if(!citation) continue; - - if(useXML) { - var citationXML = <citation index={i}/>; - } else { - output.push(i); - } - - if(citation.properties["delete"] || citation.properties.missing) { - // delete citation, or flag as missing - if(useXML) { - if(citation.properties.missing) { - if(citation.citationItems.length > 1) { - // use n tags if there are multiple items in the citation - for(var j=0; j<citation.properties.missing.length; j++) { - citationXML.missing += <missing n={citation.properties.missing[j]+1} - id={citation.properties.missingItems[j]}/>; - } - } else { - citationXML.missing = <missing id={citation.properties.missingItems[0]}/>; - } - } else { - citationXML["@delete"] = "1"; - } - output.appendChild(citationXML); - } else { - output.push("!"); - output.push("!"); - } - } else if(!useXML || !this.haveMissing) { - var field = this.getCitationField(citation); - - if(useXML) { - if(field != citation.properties.field) { - citationXML.code = field; - } - } else { - output.push(field == citation.properties.field ? "!" : field); - } - - if(citation.properties.custom) { - var citationText = citation.properties.custom; - if(useXML) { - // 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 if(useXML) { - var citationText = this.style.formatCitation(citation, "RTF"); - } else { - var citationText = this.style.formatCitation(citation, "Integration"); - } - - if(useXML) { - citationXML.text = "{\\rtf "+citationText+"}"; - } else { - output.push(citationText == "" ? " " : citationText); - } - - if(useXML) output.appendChild(citationXML); - } - } - - return output; } Zotero.Integration.Session._rtfEscapeFunction = function(aChar) { return "{\\uc0\\u"+aChar.charCodeAt(0).toString()+"}" } -/* - * loads document data from a JSON object +/** + * Loads document data from a JSON object */ Zotero.Integration.Session.prototype.loadBibliographyData = function(json) { var documentData = Zotero.JSON.unserialize(json); @@ -1570,10 +1361,12 @@ Zotero.Integration.Session.prototype.loadBibliographyData = function(json) { this.bibliographyDataHasChanged = true; } } + + this.bibliographyData = json; } -/* - * adds items in this.uncitedItems to itemSet, if they are not already there +/** + * Adds items in this.uncitedItems to itemSet, if they are not already there */ Zotero.Integration.Session.prototype.loadUncitedItems = function() { for(var itemID in this.uncitedItems) { @@ -1587,8 +1380,8 @@ Zotero.Integration.Session.prototype.loadUncitedItems = function() { } } -/* - * saves document data from a JSON object +/** + * Saves document data from a JSON object */ Zotero.Integration.Session.prototype.getBibliographyData = function() { var bibliographyData = {}; @@ -1689,6 +1482,86 @@ Zotero.Integration.Session.BibliographyEditInterface.prototype.preview = functio } /** + * 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 xmlData = <data><session id={this.sessionID} /> + <style id={this.style.styleID} hasBibliography={this.style.hasBibliography ? 1 : 0}/> + <prefs/> + </data>; + + for(var pref in this.prefs) { + xmlData.prefs.pref += <pref name={pref} value={this.prefs[pref]}/> + } + + XML.prettyPrinting = false; + var output = xmlData.toXMLString().replace("\n", "", "g"); + XML.prettyPrinting = true; + return output; +} + + +/** + * Unserializes document-specific XML + */ +Zotero.Integration.DocumentData.prototype.unserializeXML = function(xmlData) { + if(typeof xmlData == "string") { + var xmlData = new XML(xmlData); + } + + this.sessionID = xmlData.session.@id.toString(); + this.style = {"styleID":xmlData.style.@id.toString(), + "hasBibliography":(xmlData.style.@hasBibliography.toString() == 1)}; + this.prefs = {}; + for each(var pref in xmlData.prefs.children()) { + this.prefs[pref.@name.toString()] = pref.@value.toString(); + } +} + +/** + * Unserializes document-specific data, either as XML or as the string form used previously + */ +Zotero.Integration.DocumentData.prototype.unserialize = function(input) { + if(input[0] == "<" || input[1] == "<") { + this.unserializeXML(input); + } else { + const splitRe = /(^|[^\:])\:([^\:]|$)/; + + var prefParameters = []; + var splitOutput = splitRe.split(input); + for(var i=0; i<splitOutput.length; i+=3) { + prefParameters.push((splitOutput[i]+splitOutput[i+1]+splitOutput[i+2]).replace("::", ":", "g")); + } + + this.sessionID = prefParameters[0]; + this.style = {"styleID":prefParameters[1], + "hasBibliography":(prefParameters[3] == "1" || prefParameters[3] == "True")}; + this.prefs = {"fieldType":((prefParameters[5] == "1" || prefParameters[5] == "True") ? "Bookmark" : "Field")}; + if(prefParameters[2] == "note") { + if(prefParameters[4] == "1" || prefParameters[4] == "True") { + this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_ENDNOTE; + } else { + this.prefs.noteType = Components.interfaces.zoteroIntegrationDocument.NOTE_FOOTNOTE; + } + } else { + this.prefs.noteType = 0; + } + } +} + +/** * Handles mapping of item IDs to URIs */ Zotero.Integration.URIMap = function(session) { diff --git a/chrome/content/zotero/xpcom/integration_compat.js b/chrome/content/zotero/xpcom/integration_compat.js @@ -0,0 +1,345 @@ +/* + ***** 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. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Integration.Compat = new function() { + var _contentLengthRe = /[\r\n]Content-Length: *([0-9]+)/i; + var _XMLRe = /<\?[^>]+\?>/; + var _onlineObserverRegistered; + + this.sessions = {}; + + var ns = "http://www.zotero.org/namespaces/SOAP"; + this.ns = new Namespace(ns); + + this.init = init; + this.handleHeader = handleHeader; + this.handleEnvelope = handleEnvelope; + + /* + * initializes a very rudimentary web server used for SOAP RPC + */ + 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.Compat.SocketListener); + + 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(); + var output = "ERROR:"+Zotero.getString("integration.error.incompatibleVersion", Zotero.version); + var response = <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Body> + <m:{name}Response xmlns:m={this.ns}> + <output>{output}</output> + </m:{name}Response> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope>; + return _generateResponse("200 OK", 'text/xml; charset="UTF-8"', response.toXMLString()); + } else { + // new style SOAP request + var text = Zotero.getString("integration.error.incompatibleVersion", Zotero.version); + var response = <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> + <SOAP-ENV:Body> + <SOAP-ENV:Fault> + <SOAP-ENV:Code> + <SOAP-ENV:Value>XML-ENV:Sender</SOAP-ENV:Value> + <SOAP-ENV:Subcode>z:incompatibleVersion</SOAP-ENV:Subcode> + </SOAP-ENV:Code> + </SOAP-ENV:Fault> + <SOAP-ENV:Reason> + <SOAP-ENV:Text>{text}</SOAP-ENV:Text> + </SOAP-ENV:Reason> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> + return _generateResponse("500 Internal Server Error", 'text/xml; charset="UTF-8"', response.toXMLString()); + } + } + + /* + * generates the response to an HTTP request + */ + 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.Compat.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.Compat.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.Compat.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.Compat.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.Compat.DataListener.prototype.onStartRequest = function(request, context) {} + +/* + * called when a request stops + */ +Zotero.Integration.Compat.DataListener.prototype.onStopRequest = function(request, context, status) { + this.iStream.close(); + this.oStream.close(); +} + +/* + * called when new data is available + */ +Zotero.Integration.Compat.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.Compat.DataListener.prototype._headerFinished = function() { + this.headerFinished = true; + var output = Zotero.Integration.Compat.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.Compat.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.Compat.handleEnvelope(this.body); + this._requestFinished(output); + } +} + +/* + * returns HTTP data from a request + */ +Zotero.Integration.Compat.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(); + } +} +\ No newline at end of file diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -508,6 +508,18 @@ integration.deleteCitedItem.body = This reference is cited in the text of your d integration.emptyCitationWarning.title = Blank Citation integration.emptyCitationWarning.body = The citation you have specified would be empty in the currently selected style. Are you sure you want to add it? +integration.error.generic = Zotero experienced an error updating your document. +integration.error.mustInsertCitation = You must insert a citation before performing this operation. +integration.error.mustInsertBibliography = You must insert a bibliography before performing this operation. +integration.error.cannotInsertHere = Zotero fields cannot be inserted here. +integration.error.notInCitation = You must place the cursor in a Zotero citation to edit it. +integration.error.noBibliography = The current bibliographic style does not define a bibliography. If you wish to add a bibliography, please choose another style. +integration.replace = Replace this Zotero field? +integration.missingItem.single = This item no longer exists in your Zotero database. Do you want to select a substitute item? +integration.missingItem.multiple = Item %1$S in this citation no longer exists in your Zotero database. Do you want to select a substitute item? +integration.missingItem.description = Clicking "No" will delete the field codes for citations containing this item, preserving the citation text but deleting it from your bibliography. +integration.removeCodesWarning = Removing field codes will prevent Zotero from updating citations and bibliographies in this document. Are you sure you want to continue? + styles.installStyle = Install style "%1$S" from %2$S? styles.updateStyle = Update existing style "%1$S" with "%2$S" from %3$S? styles.installed = The style "%S" was installed successfully. @@ -547,4 +559,4 @@ rtfScan.saveTitle = Select a location in which to save the formatted file rtfScan.scannedFileSuffix = (Scanned) lookup.failure.title = Lookup Failed -lookup.failure.description = Zotero could not find a record for the specified identifier. Please verify the identifier and try again. +lookup.failure.description = Zotero could not find a record for the specified identifier. Please verify the identifier and try again. +\ No newline at end of file diff --git a/components/zotero-integration-service.js b/components/zotero-integration-service.js @@ -0,0 +1,132 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2009 Zotero + Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + 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 <http://www.gnu.org/licenses/>. + + Based on nsICommandLineHandler example code at + https://developer.mozilla.org/en/Chrome/Command_Line + + ***** END LICENSE BLOCK ***** +*/ + +const nsISupports = Components.interfaces.nsISupports; +const nsICategoryManager = Components.interfaces.nsICategoryManager; +const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar; +const nsICommandLine = Components.interfaces.nsICommandLine; +const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler; +const nsIFactory = Components.interfaces.nsIFactory; +const nsIModule = Components.interfaces.nsIModule; +const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher; + +const clh_contractID = "@mozilla.org/commandlinehandler/general-startup;1?type=zotero-integration"; +const clh_CID = Components.ID("{531828f8-a16c-46be-b9aa-14845c3b010f}"); +const clh_category = "m-zotero-integration"; + +/** + * The XPCOM component that implements nsICommandLineHandler. + * It also implements nsIFactory to serve as its own singleton factory. + */ +const ZoteroIntegrationCommandLineHandler = { + Zotero : null, + + /* nsISupports */ + QueryInterface : function(iid) { + if(iid.equals(nsICommandLineHandler) || + iid.equals(nsIFactory) || + iid.equals(nsISupports)) return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + /* nsICommandLineHandler */ + handle : function(cmdLine) { + var agent = cmdLine.handleFlagWithParam("ZoteroIntegrationAgent", false); + var command = cmdLine.handleFlagWithParam("ZoteroIntegrationCommand", false); + if(agent && command) { + if(!this.Zotero) this.Zotero = Components.classes["@zotero.org/Zotero;1"] + .getService(Components.interfaces.nsISupports).wrappedJSObject; + var Zotero = this.Zotero; + // Not quite sure why this is necessary to get the appropriate scoping + var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + timer.initWithCallback({notify:function() { Zotero.Integration.execCommand(agent, command) }}, 0, + Components.interfaces.nsITimer.TYPE_ONE_SHOT); + } + }, + + /* nsIFactory */ + createInstance : function(outer, iid) { + if (outer != null) throw Components.results.NS_ERROR_NO_AGGREGATION; + return this.QueryInterface(iid); + }, + + lockFactory : function(lock) { + /* no-op */ + } +}; + +/** + * The XPCOM glue that implements nsIModule + */ +const ZoteroIntegrationModule = { + /* nsISupports */ + QueryInterface : function(iid) { + if(iid.equals(nsIModule) || iid.equals(nsISupports)) return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + + /* nsIModule */ + getClassObject : function(compMgr, cid, iid) { + if(cid.equals(clh_CID)) return ZoteroIntegrationCommandLineHandler.QueryInterface(iid); + throw Components.results.NS_ERROR_NOT_REGISTERED; + }, + + registerSelf : function(compMgr, fileSpec, location, type) { + compMgr.QueryInterface(nsIComponentRegistrar); + + compMgr.registerFactoryLocation(clh_CID, + "myAppHandler", + clh_contractID, + fileSpec, + location, + type); + + var catMan = Components.classes["@mozilla.org/categorymanager;1"]. + getService(nsICategoryManager); + catMan.addCategoryEntry("command-line-handler", + clh_category, + clh_contractID, true, true); + }, + + unregisterSelf : function(compMgr, location, type) { + compMgr.QueryInterface(nsIComponentRegistrar); + compMgr.unregisterFactoryLocation(clh_CID, location); + + var catMan = Components.classes["@mozilla.org/categorymanager;1"]. + getService(nsICategoryManager); + catMan.deleteCategoryEntry("command-line-handler", clh_category); + }, + + canUnload : function (compMgr) { + return true; + } +}; + +/* The NSGetModule function is the magic entry point that XPCOM uses to find what XPCOM objects + * this component provides + */ +function NSGetModule(comMgr, fileSpec){ return ZoteroIntegrationModule; } diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -50,6 +50,7 @@ var xpcomFiles = [ 'id', 'ingester', 'integration', + 'integration_compat', 'itemTreeView', 'mime', 'mimeTypeHandler', diff --git a/idl/zoteroIntegration.idl b/idl/zoteroIntegration.idl @@ -0,0 +1,151 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2009 Zotero + Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + 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 <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +#include "nsISupports.idl" +#include "nsISimpleEnumerator.idl" + +[scriptable, uuid(aedb37a0-48bb-11de-8a39-0800200c9a66)] +interface zoteroIntegrationField : nsISupports +{ + /** + * Deletes this field and its contents. + */ + void delete(); + + /** + * Selects this field. + */ + void select(); + + /** + * Removes this field, but maintains the field's contents. + */ + void removeCode(); + + /** + * Sets the text inside this field to a specified plain text string or pseudo-RTF formatted text + * string. + */ + void setText(in wstring text, in boolean isRich); + + /** + * This field's code. + */ + wstring getCode(); + void setCode(in wstring code); + + /** + * This field's note index, if it is in a footnote or endnote; otherwise zero. + */ + unsigned long getNoteIndex(); + + /** + * Returns true if this field and the passed field are actually references to the same field. + */ + boolean equals(in zoteroIntegrationField field); +}; + +/** + * The zoteroIntegrationDocument interface corresponds to a single word processing document. + */ +[scriptable, uuid(be1c5c1f-9ed2-4154-98fb-822d1fede569)] +interface zoteroIntegrationDocument : nsISupports +{ + /** + * Displays a dialog in the word processing application + */ + short displayAlert(in wstring dialogText, in unsigned short icon, + in unsigned short buttons); + + /** + * Brings this document to the foreground (if necessary to return after displaying a dialog) + */ + void activate(); + + /** + * Determines whether a field can be inserted at the current position. + */ + boolean canInsertField(in string fieldType); + + /** + * Returns the field in which the cursor resides, or NULL if none. + */ + zoteroIntegrationField cursorInField(in string fieldType); + + /** + * The document data property from the current document. + */ + wstring getDocumentData(); + void setDocumentData(in wstring data); + + /** + * Inserts a field at the given position and initializes the field object. + */ + zoteroIntegrationField insertField(in string fieldType, in unsigned short noteType); + + /** + * Inserts an uninitialized field object at the given position + */ + nsISimpleEnumerator getFields(in string fieldType); + + /** + * Converts all fields in a document to a different fieldType or noteType + */ + void convert(in nsISimpleEnumerator fields, in string toFieldType, + [array, size_is(count)] in unsigned short toNoteType, in unsigned long count); + + /** + * Runs on function completion to clean up everything integration played with. + */ + void cleanup(); + + const unsigned short DIALOG_ICON_STOP = 0; + const unsigned short DIALOG_ICON_NOTICE = 1; + const unsigned short DIALOG_ICON_CAUTION = 2; + + const unsigned short DIALOG_BUTTONS_OK = 0; + const unsigned short DIALOG_BUTTONS_OK_CANCEL = 1; + const unsigned short DIALOG_BUTTONS_YES_NO = 2; + const unsigned short DIALOG_BUTTONS_YES_NO_CANCEL = 3; + + const unsigned short NOTE_FOOTNOTE = 1; + const unsigned short NOTE_ENDNOTE = 2; +}; + +/** + * The zoteroIntegrationApplication interface corresponds to a word processing application. + */ +[scriptable, uuid(7b258e57-20cf-4a73-8420-5d06a538c25e)] +interface zoteroIntegrationApplication : nsISupports +{ + readonly attribute ACString primaryFieldType; + readonly attribute ACString secondaryFieldType; + + /** + * The active document. + */ + zoteroIntegrationDocument getActiveDocument(); +}; + +/////////////////////////////////////////////////////////////////////////////// diff --git a/license_integration.txt b/license_integration.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>.