<commit_before>/*
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org
    
    This file is part of Zotero.
    
    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
    
    ***** END LICENSE BLOCK *****
*/

const RESELECT_KEY_URI = 1;
const RESELECT_KEY_ITEM_KEY = 2;
const RESELECT_KEY_ITEM_ID = 3;
const DATA_VERSION = 3;

// this is used only for update checking
const INTEGRATION_PLUGINS = ["<EMAIL>",
	"<EMAIL>", "<EMAIL>"];

Zotero.Integration = new function() {
	const INTEGRATION_MIN_VERSIONS = ["3.1.6.SVN", "3.5b1.SVN", "3.1.2.SVN"];
	
	var _fifoFile = null;
	var _tmpFile = null;
	var _osascriptFile;
	var _inProgress = false;
	var _integrationVersionsOK = null;
	
	// these need to be global because of GC
	var _updateTimer;
	
	this.sessions = {};
	
	/**
	 * Initializes the pipe used for integration on non-Windows platforms.
	 */
	this.init = function() {
		// We only use an integration pipe on OS X.
		// On Linux, we use the alternative communication method in the OOo plug-in
		// On Windows, we use a command line handler for integration. See
		// components/zotero-integration-service.js for this implementation.
		if(!Zotero.isMac) return;
	
		// Determine where to put the pipe
		// on OS X, first try /Users/Shared for those who can't put pipes in their home
		// directories
		_fifoFile = Components.classes["@mozilla.org/file/local;1"].
			createInstance(Components.interfaces.nsILocalFile);
		_fifoFile.initWithPath("/Users/Shared");
		
		if(_fifoFile.exists() && _fifoFile.isDirectory() && _fifoFile.isWritable()) {
			var logname = Components.classes["@mozilla.org/process/environment;1"].
				getService(Components.interfaces.nsIEnvironment).
				get("LOGNAME");
			_fifoFile.append(".zoteroIntegrationPipe_"+logname);
		} else {
			_fifoFile = null;
		}
		
		if(!_fifoFile) {
			// on other platforms, or as a fallback, use home directory
			_fifoFile = Components.classes["@mozilla.org/file/directory_service;1"].
				getService(Components.interfaces.nsIProperties).
				get("Home", Components.interfaces.nsIFile);
			_fifoFile.append(".zoteroIntegrationPipe");
		}
		
		// destroy old pipe, if one exists
		try {
			if(_fifoFile.exists()) {
				Zotero.IPC.safePipeWrite(_fifoFile, "Zotero shutdown\n");
				_fifoFile.remove(false);
			}
		} catch (e) {
			// if pipe can't be deleted, log an error
			Zotero.debug("Error removing old integration pipe", 1);
			Zotero.logError(e);
			Components.utils.reportError(
				"Zotero word processor integration initialization failed. "
					+ "See http://forums.zotero.org/discussion/12054/#Item_10 "
					+ "for instructions on correcting this problem."
			);
			
			// can attempt to delete on OS X
			try {
				var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
					.getService(Components.interfaces.nsIPromptService);
				var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe"));
				if(!deletePipe) return;
				let escapedFifoFile = _fifoFile.path.replace("'", "'\\''");
				_executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true);
				if(_fifoFile.exists()) return;
			} catch(e) {
				Zotero.logError(e);
				return;
			}
		}
		
		// try to initialize pipe
		try {
			Zotero.IPC.Pipe.initPipeListener(_fifoFile, _parseIntegrationPipeCommand);
		} catch(e) {
			Zotero.logError(e);
		}
		
		_updateTimer = Components.classes["@mozilla.org/timer;1"].
			createInstance(Components.interfaces.nsITimer);
		_updateTimer.initWithCallback({"notify":_checkPluginVersions}, 1000,
			Components.interfaces.nsITimer.TYPE_ONE_SHOT);
	}
	
	function _checkPluginVersions() {
		if(_updateTimer) _updateTimer = undefined;
		
		var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
			.getService(Components.interfaces.nsIVersionComparator);
		var addonsChecked = false;
		var success = true;
		function _checkAddons(addons) {
			addonsChecked = true;
			for(var i in addons) {
				var addon = addons[i];
				if(!addon) continue;
				if(addon.userDisabled) continue;
				
				if(verComp.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) {
					_integrationVersionsOK = false;
					Zotero.Integration.activate();
					var msg = Zotero.getString(
						"integration.error.incompatibleVersion2",
						[Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]]
					);
					Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
						.getService(Components.interfaces.nsIPromptService)
						.alert(null, Zotero.getString("integration.error.title"), msg);
					success = false;
					throw msg;
				}
			}
			_integrationVersionsOK = true;
		}
	
		if(Zotero.isFx4) {
			Components.utils.import("resource://gre/modules/AddonManager.jsm");
			AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, _checkAddons);
			while(!addonsChecked) Zotero.mainThread.processNextEvent(true);
		} else {
			var extMan = Components.classes['@mozilla.org/extensions/manager;1'].
				getService(Components.interfaces.nsIExtensionManager);
			_checkAddons([extMan.getItemForID(id) for each(id in INTEGRATION_PLUGINS)]);
		}
		
		return success;
	}
	
	/**
	 * Executes an integration command, first checking to make sure that versions are compatible
	 */
	this.execCommand = function execCommand(agent, command, docId) {
		if(_inProgress) {
			Zotero.Integration.activate();
			Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command);
			return;
		}
		_inProgress = true;
		
		// Check integration component versions
		if(_checkPluginVersions()) {
			_callIntegration(agent, command, docId);
		} else {
			inProgress = false;
		}
	}
	
	/**
	 * Parses a command received from the integration pipe
	 */
	function _parseIntegrationPipeCommand(string) {
		if(string != "") {
			// exec command if possible
			var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/);
			if(parts) {
				var agent = parts[1].toString();
				var cmd = parts[2].toString();
				var document = parts[3] ? parts[3].toString() : null;
				Zotero.Integration.execCommand(agent, cmd, document);
			} else {
				Components.utils.reportError("Zotero: Invalid integration input received: "+string);
			}
		}
	}
	
	/**
	 * Calls the Integration applicatoon
	 */
	function _callIntegration(agent, command, docId) {
		// Try to load the appropriate Zotero component; otherwise display an error using the alert
		// service
		try {
			var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1";
			Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : ""));
			var application = Components.classes[componentClass]
				.getService(Components.interfaces.zoteroIntegrationApplication);
		} catch(e) {
			_inProgress = false;
			Zotero.Integration.activate();
			Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
				.getService(Components.interfaces.nsIPromptService)
				.alert(null, Zotero.getString("integration.error.title"),
					Zotero.getString("integration.error.notInstalled"));
			throw e;
		}
		
		// Try to execute the command; otherwise display an error in alert service or word processor
		// (depending on what is possible)
		var integration, document;
		try {
			document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument());
			integration = new Zotero.Integration.Document(application, document);
			integration[command]();
			integration.cleanup();
		} catch(e) {
			if(integration) {
				try {
					integration.cleanup();
				} catch(e) {
					Components.utils.reportError(e);
				}
			}
			
			if(!(e instanceof Zotero.Integration.UserCancelledException)) {
				try {
					var displayError = null;
					if(e instanceof Zotero.Integration.DisplayException) {
						displayError = e.toString();
					} else {
						// check to see whether there's a pyxpcom error in the console, since it doesn't
						// get thrown directly
						var message = "";
						
						var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
							.getService(Components.interfaces.nsIConsoleService);
						
						var messages = {};
						consoleService.getMessageArray(messages, {});
						messages = messages.value;
						if(messages && messages.length) {
							var lastMessage = messages[messages.length-1];
							try {
								var error = lastMessage.QueryInterface(Components.interfaces.nsIScriptError);
							} catch(e2) {
								if(lastMessage.message && lastMessage.message.substr(0, 12) == "ERROR:xpcom:") {
									// print just the last line of the message, but re-throw the rest
									message = lastMessage.message.substr(0, lastMessage.message.length-1);
									message = "\n"+message.substr(message.lastIndexOf("\n"))
								}
							}
						}
						
						if(!message && typeof(e) == "object" && e.message) message = "\n\n"+e.message;
						
						if(message != "\n\nExceptionAlreadyDisplayed") {
							displayError = Zotero.getString("integration.error.generic")+message;
						}
						Zotero.debug(e);
					}
					
					if(displayError) {
						if(integration) {
							integration._doc.displayAlert(displayError,
									Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP,
									Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK);
						} else {
							Zotero.Integration.activate();
							Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
								.getService(Components.interfaces.nsIPromptService)
								.alert(null, Zotero.getString("integration.error.title"), displayError);
						}
					}
				} finally {
					throw e;
				}
			}
		} finally {
			_inProgress = false;
		}
	}
	
	/**
	 * Activates Firefox
	 */
	this.activate = function(win) {
		if(Zotero.isMac) {
			const BUNDLE_IDS = {
				"Zotero":"org.zotero.zotero",
				"Firefox":"org.mozilla.firefox",
				"Minefield":"org.mozilla.minefield"
			};
			
			if(Zotero.isFx4 && win) {
				const carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon");
				/*
				 * struct ProcessSerialNumber {
				 *    unsigned long highLongOfPSN;
				 *    unsigned long lowLongOfPSN;
				 * };
				 */
				const ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", 
					[{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]);
					
				/*
				 * OSStatus SetFrontProcessWithOptions (
				 *    const ProcessSerialNumber *inProcess,
				 *    OptionBits inOptions
				 * );
				 */
				const SetFrontProcessWithOptions = carbon.declare("SetFrontProcessWithOptions",
					ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, ctypes.uint32_t);
				
				var psn = new ProcessSerialNumber();
				psn.highLongOfPSN = 0;
				psn.lowLongOfPSN = 2 // kCurrentProcess
				
				win.addEventListener("load", function() {
					var res = SetFrontProcessWithOptions(
						psn.address(),
						1 // kSetFrontProcessFrontWindowOnly = (1 << 0)
					);
					carbon.close();
				}, false);
			} else {
				if(Zotero.oscpu == "PPC Mac OS X 10.4" || Zotero.oscpu == "Intel Mac OS X 10.4"
				   || !BUNDLE_IDS[Zotero.appName]) {
					// 10.4 doesn't support "tell application id"
					_executeAppleScript('tell application "'+Zotero.appName+'" to activate');
				} else {
					_executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate');
				}
			}
		}
	}
	
	/**
	 * Runs an AppleScript on OS X
	 *
	 * @param script {String}
	 * @param block {Boolean} Whether the script should block until the process is finished.
	 */
	function _executeAppleScript(script, block) {
		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);
			try {
				proc.run(!!block, ['-e', script], 2);
			} catch(e) {}
		}
	}
}

/**
 * An exception thrown when a document contains an item that no longer exists in the current document.
 *
 * @param reselectKeys {Array} Keys representing the missing item
 * @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants)
 * @param citationIndex {Integer} The index of the missing item within the citation cluster
 * @param citationLength {Integer} The number of items cited in this citation cluster
 */
Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) {
	this.reselectKeys = reselectKeys;
	this.reselectKeyType = reselectKeyType;
	this.citationIndex = citationIndex;
	this.citationLength = citationLength;
}
Zotero.Integration.MissingItemException.prototype.name = "MissingItemException";
Zotero.Integration.MissingItemException.prototype.message = "An item in this document is missing from your Zotero library.";
Zotero.Integration.MissingItemException.prototype.toString = function() { return this.message; };

Zotero.Integration.UserCancelledException = function() {};
Zotero.Integration.UserCancelledException.prototype.name = "UserCancelledException";
Zotero.Integration.UserCancelledException.prototype.message = "User cancelled document update.";
Zotero.Integration.UserCancelledException.prototype.toString = function() { return this.message; };

Zotero.Integration.DisplayException = function(name, params) {
	this.name = name;
	this.params = params ? params : [];
};
Zotero.Integration.DisplayException.prototype.toString = function() { return Zotero.getString("integration.error."+this.name, this.params); };

Zotero.Integration.CorruptFieldException = function(corruptFieldString) {
	this.corruptFieldString = corruptFieldString;
}
Zotero.Integration.CorruptFieldException.prototype.name = "CorruptFieldException";
Zotero.Integration.CorruptFieldException.prototype.message = "A field code in this document is corrupted.";
Zotero.Integration.CorruptFieldException.prototype.toString = function() { return this.message+" "+this.corruptFieldString.toSource(); }

const INTEGRATION_TYPE_ITEM = 1;
const INTEGRATION_TYPE_BIBLIOGRAPHY = 2;
const INTEGRATION_TYPE_TEMP = 3;

// Placeholder for an empty bibliography
const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}";

/**
 * All methods for interacting with a document
 * @constructor
 */
Zotero.Integration.Document = function(app, doc) {
	this._app = app;
	this._doc = doc;
}

/**
 * Gets the type of the field
 */
Zotero.Integration.Document.prototype._getCodeTypeAndContent = function(rawCode) {
	for each(var code in ["ITEM", "CITATION"]) {
		if(rawCode.substr(0, code.length) === code) {
			return [INTEGRATION_TYPE_ITEM, rawCode.substr(code.length+1)];
		}
	}
	
	if(rawCode.substr(0, 4) === "BIBL") {
		return [INTEGRATION_TYPE_BIBLIOGRAPHY, rawCode.substr(5)];
	}
	
	if(rawCode.substr(0, 4) === "TEMP") {
		return [INTEGRATION_TYPE_TEMP, rawCode.substr(5)];
	}
	
	return [null, rawCode];
}

/**
 * Creates a new session
 * @param data {Zotero.Integration.DocumentData} Document data for new session
 */
Zotero.Integration.Document.prototype._createNewSession = function(data) {
	data.sessionID = Zotero.randomString();
	var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session();
	return session;
}

/**
 * Gets preferences for a document
 * @param require {Boolean} Whether an error should be thrown if no preferences or fields exist 
 *                          (otherwise, the set doc prefs dialog is shown)
 * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no
 *                                     preferences exist
 */
Zotero.Integration.Document.prototype._getSession = function(require, dontRunSetDocPrefs) {
	this._reloadSession = false;
	var dataString = this._doc.getDocumentData();
	if(!dataString) {
		var haveFields = false;
		var data = new Zotero.Integration.DocumentData();
		
		if(require) {
			// check to see if fields already exist
			for each(var fieldType in [this._app.primaryFieldType, this._app.secondaryFieldType]) {
				var fields = this._doc.getFields(this._app.primaryFieldType);
				if(fields.hasMoreElements()) {
					data.prefs.fieldType = this._app.primaryFieldType;
					haveFields = true;
					break;
				}
			}
			
			// if no fields, throw an error
			if(!haveFields) {
				throw new Zotero.Integration.DisplayException("mustInsertCitation");
			} else {
				Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields");
			}
		}
		
		// Set doc prefs if no data string yet
		this._session = this._createNewSession(data);
		this._session.setData(data);
		if(dontRunSetDocPrefs) return false;
		
		try {
			var ret = this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType);
		} finally {
			this._doc.activate();
		}
		
		// save doc prefs in doc
		this._doc.setDocumentData(this._session.data.serializeXML());
		
		if(haveFields) {
			this._reloadSession = true;
		}
	} else {
		var data = new Zotero.Integration.DocumentData(dataString);
		if(data.dataVersion < DATA_VERSION) {
			if(data.dataVersion == 1
					&& data.prefs.fieldType == "Field"
					&& this._app.primaryFieldType == "ReferenceMark") {
				// Converted OOo docs use ReferenceMarks, not fields
				data.prefs.fieldType = "ReferenceMark";
			}
			
			var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning"),
				Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING,
				Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL);
			if(!warning) throw new Zotero.Integration.UserCancelledException();
		} else if(data.dataVersion > DATA_VERSION) {
			throw new Zotero.Integration.DisplayException("newerDocumentVersion", [data.zoteroVersion, Zotero.version]);
		}
		
		if(data.prefs.fieldType !== this._app.primaryFieldType
				&& data.prefs.fieldType !== this._app.secondaryFieldType) {
			throw new Zotero.Integration.DisplayException("fieldTypeMismatch");
		}
		
		if(Zotero.Integration.sessions[data.sessionID]) {
			this._session = Zotero.Integration.sessions[data.sessionID];
		} else {
			this._session = this._createNewSession(data);
			try {
				this._session.setData(data);
			} catch(e) {
				// make sure style is defined
				if(e instanceof Zotero.Integration.DisplayException && e.name === "invalidStyle") {
					try {
						this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType);
					} finally {
						this._doc.activate();
					}
				} else {
					throw e;
				}
			}
			this._doc.setDocumentData(this._session.data.serializeXML());
			
			this._reloadSession = true;
		}
	}
	
	this._session.resetRequest(this);
	return !!dataString;
}

/**
 * Gets all fields for a document
 * @param require {Boolean} Whether an error should be thrown if no fields exist
 * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no
 *                                     preferences exist
 */
Zotero.Integration.Document.prototype._getFields = function(require, dontRunSetDocPrefs) {
	if(this._fields) return;
	if(!this._session && !this._getSession(require, dontRunSetDocPrefs)) return;
	
	var getFieldsTime = (new Date()).getTime();
	var fields = this._doc.getFields(this._session.data.prefs['fieldType']);
	this._fields = [];
	while(fields.hasMoreElements()) {
		this._fields.push(fields.getNext().QueryInterface(Components.interfaces.zoteroIntegrationField));
	}
	var endTime = (new Date()).getTime();
	if(Zotero.Debug.enabled) {
		Zotero.debug("Integration: got "+this._fields.length+" fields in "+
			(endTime-getFieldsTime)/1000+"; "+
			1000/((endTime-getFieldsTime)/this._fields.length)+" fields/second");
	}
	
	if(require && !this._fields.length) {
		throw new Zotero.Integration.DisplayException("mustInsertCitation");
	}
	
	return;
}

/**
 * Checks that it is appropriate to add fields to the current document at the current
 * positon, then adds one.
 */
Zotero.Integration.Document.prototype._addField = function(note) {
	// Get citation types if necessary
	if(!this._doc.canInsertField(this._session.data.prefs['fieldType'])) {
		throw new Zotero.Integration.DisplayException("cannotInsertHere");
		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;
}

/**
 * Shows an error if a field code is corrupted
 * @param {Exception} e The exception thrown
 * @param {Field} field The Zotero field object
 * @param {Integer} i The field index
 */
Zotero.Integration.Document.prototype._showCorruptFieldError = function(e, field, i) {
	var msg = Zotero.getString("integration.corruptField")+'\n\n'+
			  Zotero.getString('integration.corruptField.description');
	field.select();
	var result = this._doc.displayAlert(msg,
		Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
		Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL);
	
	if(result == 0) {
		throw e;
	} else if(result == 1) {		// No
		this._removeCodeFields.push(i);
	} else {
		// Display reselect edit citation dialog
		var added = this._session.editCitation(i, field.getNoteIndex());
		if(added) {
			this._doc.activate();
		} else {
			throw new Zotero.Integration.UserCancelledException();
		}
	}
}

/**
 * Loads existing citations and bibliographies out of a document, and creates or edits fields
 */
Zotero.Integration.Document.prototype._updateSession = function(newField, editField) {
	var deleteKeys = {};
	this._deleteFields = [];
	this._removeCodeFields = [];
	this._bibliographyFields = [];
	var bibliographyData = "";
	
	// first collect entire bibliography
	this._getFields();
	var editFieldIndex = false;
	var collectFieldsTime = (new Date()).getTime();
	for(var i in this._fields) {
		var field = this._fields[i];
		
		if(editField && field.equals(editField)) {
			editFieldIndex = i;
		} else {
			try {
				var fieldCode = field.getCode();
			} catch(e) {
				this._showCorruptFieldError(e, field, i);
			}
			
			var [type, content] = this._getCodeTypeAndContent(fieldCode);
			if(type === INTEGRATION_TYPE_ITEM) {
				var noteIndex = (this._session.styleClass == "note" ? field.getNoteIndex() : 0);
				try {
					this._session.addCitation(i, noteIndex, content);
				} 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+1).toString());
							}
							msg += '\n\n'+Zotero.getString('integration.missingItem.description');
							field.select();
							var result = this._doc.displayAlert(msg, 1, 3);
							if(result == 0) {			// Cancel
								throw new Zotero.Integration.UserCancelledException();
							} 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
								this._session.reselectItem(e);
								// Now try again
								this._session.addCitation(i, field.getNoteIndex(), content);
								this._doc.activate();
							}
						}
					} else if(e instanceof Zotero.Integration.CorruptFieldException) {
						this._showCorruptFieldError(e, field, i);
					} else {
						throw e;
					}
				}
			} else if(type === INTEGRATION_TYPE_BIBLIOGRAPHY) {
				this._bibliographyFields.push(field);
				if(!this._session.bibliographyData && !bibliographyData) {
					bibliographyData = content;
				}
			} else if(type === INTEGRATION_TYPE_TEMP) {
				if(newField && newField.equals(field)) {
					editFieldIndex = i;
					editField = field;
				} else {
					this._deleteFields.push(i);
				}
			}
		}
	}
	var endTime = (new Date()).getTime();
	if(Zotero.Debug.enabled) {
		Zotero.debug("Integration: collected "+this._fields.length+" fields in "+
			(endTime-collectFieldsTime)/1000+"; "+
			1000/((endTime-collectFieldsTime)/this._fields.length)+" fields/second");
	}
	
	// load uncited items from bibliography
	if(bibliographyData && !this._session.bibliographyData) {
		try {
			this._session.loadBibliographyData(bibliographyData);
		} catch(e) {
			if(e instanceof Zotero.Integration.CorruptFieldException) {
				var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+
						  Zotero.getString('integration.corruptBibliography.description');
				var result = this._doc.displayAlert(msg, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL);
				if(result == 0) {
					throw e;
				} else {
					bibliographyData = "";
					this._session.bibliographyHasChanged = true;
					this._session.bibliographyDataHasChanged = true;
				}
			} else {
				throw e;
			}
		}
	}
	
	// if we are reloading this session, assume no item IDs to be updated except for edited items
	if(this._reloadSession) {
		//this._session.restoreProcessorState(); TODO doesn't appear to be working properly
		this._session.updateUpdateIndices();
		var deleteCitations = this._session.updateCitations();
		this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]);
		this._session.updateIndices = {};
		this._session.updateItemIDs = {};
		this._session.bibliographyHasChanged = false;
	}
	
	// create new citation or edit existing citation
	if(editFieldIndex) { 
		var [type, editFieldCode] = this._getCodeTypeAndContent(editField.getCode());
		var editCitation = editFieldCode ? this._session.unserializeCitation(editFieldCode, editFieldIndex) : null;
		
		var editNoteIndex = editField.getNoteIndex();
		var added = this._session.editCitation(editFieldIndex, editNoteIndex, editCitation);
		this._doc.activate();
		
		if(!added) {
			if(editFieldCode) {	// cancelled editing; just add as if nothing happened
				this._session.addCitation(editFieldIndex, editNoteIndex, editCitation);
			} else {			// cancelled creation; delete the citation
				this._session.deleteCitation(editFieldIndex);
			}
		}
	}
}

/**
 * Updates bibliographies and fields within a document
 * @param {Boolean} forceCitations Whether to regenerate all citations
 * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries
 * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been 
 *	modified since they were created, instead of showing a warning
 */
Zotero.Integration.Document.prototype._updateDocument = function(forceCitations, forceBibliography,
		ignoreCitationChanges) {
	// update citations
	this._session.updateUpdateIndices(forceCitations);
	var deleteCitations = this._session.updateCitations();
	this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]);
	for(var i in this._session.updateIndices) {
		var citation = this._session.citationsByIndex[i];
		var field = this._fields[i];
		
		// If there is no citation, we're deleting it, or we shouldn't update it, ignore it
		if(!citation || deleteCitations[i]) continue;
		
		if(!citation.properties.dontUpdate) {
			var isRich = false;
			var formattedCitation = citation.properties.custom
				? citation.properties.custom : this._session.citationText[i];
			
			if(formattedCitation.indexOf("\\") !== -1) {
				// need to set text as RTF
				formattedCitation = "{\\rtf "+formattedCitation+"}"
				isRich = true;
			}
			
			if(forceCitations || citation.properties.formattedCitation !== formattedCitation) {
				// Check if citation has been manually modified
				if(!ignoreCitationChanges && citation.properties.plainCitation) {
					var plainCitation = field.getText();
					if(plainCitation !== citation.properties.plainCitation) {
						// Citation manually modified; ask user if they want to save changes
						field.select();
						var result = this._doc.displayAlert(
							Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO);
						if(result) {
							citation.properties.dontUpdate = true;
						}
					}
				}
				
				if(!citation.properties.dontUpdate) {
					field.setText(formattedCitation, isRich);
					
					citation.properties.formattedCitation = formattedCitation;
					citation.properties.plainCitation = field.getText();
				}
			}
		}
		
		var fieldCode = this._session.getCitationField(citation);
		if(fieldCode != citation.properties.field) {
			field.setCode(
				(this._session.data.prefs.storeReferences ? "ITEM CSL_CITATION" : "ITEM")
				+" "+fieldCode);
			
			if(this._session.data.prefs.fieldType === "ReferenceMark" && isRich
					&& !citation.properties.dontUpdate) {
				// For ReferenceMarks with formatting, we need to set the text again, because
				// setting the field code removes formatting from the mark. I don't like this.
				field.setText(citation.properties.custom ? citation.properties.custom 
					: citation.properties.formattedCitation, isRich);
			}
		}
	}
	
	// update bibliographies
	if(this._bibliographyFields.length	 				// if blbliography exists
			&& (this._session.bibliographyHasChanged	// and bibliography changed
			|| forceBibliography)) {					// or if we should generate regardless of changes
		if(forceBibliography || this._session.bibliographyDataHasChanged) {
			var bibliographyData = this._session.getBibliographyData();
			for each(var field in this._bibliographyFields) {
				field.setCode("BIBL "+bibliographyData
					+(this._session.data.prefs.storeReferences ? " CSL_BIBLIOGRAPHY" : ""));
			}
		}
		
		// get bibliography and format as RTF
		var bib = this._session.getBibliography();
		
		var bibliographyText = "";
		if(bib) {
			bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend;
			
			// if bibliography style not set, set it
			if(!this._session.data.style.bibliographyStyleHasBeenSet) {
				var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib);
				<commit_msg>Fix adding formatted custom citations in OpenOffice.org<commit_after>/*
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org
    
    This file is part of Zotero.
    
    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
    
    ***** END LICENSE BLOCK *****
*/

const RESELECT_KEY_URI = 1;
const RESELECT_KEY_ITEM_KEY = 2;
const RESELECT_KEY_ITEM_ID = 3;
const DATA_VERSION = 3;

// this is used only for update checking
const INTEGRATION_PLUGINS = ["<EMAIL>",
	"<EMAIL>", "<EMAIL>"];

Zotero.Integration = new function() {
	const INTEGRATION_MIN_VERSIONS = ["3.1.6.SVN", "3.5b1.SVN", "3.1.2.SVN"];
	
	var _fifoFile = null;
	var _tmpFile = null;
	var _osascriptFile;
	var _inProgress = false;
	var _integrationVersionsOK = null;
	
	// these need to be global because of GC
	var _updateTimer;
	
	this.sessions = {};
	
	/**
	 * Initializes the pipe used for integration on non-Windows platforms.
	 */
	this.init = function() {
		// We only use an integration pipe on OS X.
		// On Linux, we use the alternative communication method in the OOo plug-in
		// On Windows, we use a command line handler for integration. See
		// components/zotero-integration-service.js for this implementation.
		if(!Zotero.isMac) return;
	
		// Determine where to put the pipe
		// on OS X, first try /Users/Shared for those who can't put pipes in their home
		// directories
		_fifoFile = Components.classes["@mozilla.org/file/local;1"].
			createInstance(Components.interfaces.nsILocalFile);
		_fifoFile.initWithPath("/Users/Shared");
		
		if(_fifoFile.exists() && _fifoFile.isDirectory() && _fifoFile.isWritable()) {
			var logname = Components.classes["@mozilla.org/process/environment;1"].
				getService(Components.interfaces.nsIEnvironment).
				get("LOGNAME");
			_fifoFile.append(".zoteroIntegrationPipe_"+logname);
		} else {
			_fifoFile = null;
		}
		
		if(!_fifoFile) {
			// on other platforms, or as a fallback, use home directory
			_fifoFile = Components.classes["@mozilla.org/file/directory_service;1"].
				getService(Components.interfaces.nsIProperties).
				get("Home", Components.interfaces.nsIFile);
			_fifoFile.append(".zoteroIntegrationPipe");
		}
		
		// destroy old pipe, if one exists
		try {
			if(_fifoFile.exists()) {
				Zotero.IPC.safePipeWrite(_fifoFile, "Zotero shutdown\n");
				_fifoFile.remove(false);
			}
		} catch (e) {
			// if pipe can't be deleted, log an error
			Zotero.debug("Error removing old integration pipe", 1);
			Zotero.logError(e);
			Components.utils.reportError(
				"Zotero word processor integration initialization failed. "
					+ "See http://forums.zotero.org/discussion/12054/#Item_10 "
					+ "for instructions on correcting this problem."
			);
			
			// can attempt to delete on OS X
			try {
				var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
					.getService(Components.interfaces.nsIPromptService);
				var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe"));
				if(!deletePipe) return;
				let escapedFifoFile = _fifoFile.path.replace("'", "'\\''");
				_executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true);
				if(_fifoFile.exists()) return;
			} catch(e) {
				Zotero.logError(e);
				return;
			}
		}
		
		// try to initialize pipe
		try {
			Zotero.IPC.Pipe.initPipeListener(_fifoFile, _parseIntegrationPipeCommand);
		} catch(e) {
			Zotero.logError(e);
		}
		
		_updateTimer = Components.classes["@mozilla.org/timer;1"].
			createInstance(Components.interfaces.nsITimer);
		_updateTimer.initWithCallback({"notify":_checkPluginVersions}, 1000,
			Components.interfaces.nsITimer.TYPE_ONE_SHOT);
	}
	
	function _checkPluginVersions() {
		if(_updateTimer) _updateTimer = undefined;
		
		var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
			.getService(Components.interfaces.nsIVersionComparator);
		var addonsChecked = false;
		var success = true;
		function _checkAddons(addons) {
			addonsChecked = true;
			for(var i in addons) {
				var addon = addons[i];
				if(!addon) continue;
				if(addon.userDisabled) continue;
				
				if(verComp.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) {
					_integrationVersionsOK = false;
					Zotero.Integration.activate();
					var msg = Zotero.getString(
						"integration.error.incompatibleVersion2",
						[Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]]
					);
					Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
						.getService(Components.interfaces.nsIPromptService)
						.alert(null, Zotero.getString("integration.error.title"), msg);
					success = false;
					throw msg;
				}
			}
			_integrationVersionsOK = true;
		}
	
		if(Zotero.isFx4) {
			Components.utils.import("resource://gre/modules/AddonManager.jsm");
			AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, _checkAddons);
			while(!addonsChecked) Zotero.mainThread.processNextEvent(true);
		} else {
			var extMan = Components.classes['@mozilla.org/extensions/manager;1'].
				getService(Components.interfaces.nsIExtensionManager);
			_checkAddons([extMan.getItemForID(id) for each(id in INTEGRATION_PLUGINS)]);
		}
		
		return success;
	}
	
	/**
	 * Executes an integration command, first checking to make sure that versions are compatible
	 */
	this.execCommand = function execCommand(agent, command, docId) {
		if(_inProgress) {
			Zotero.Integration.activate();
			Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command);
			return;
		}
		_inProgress = true;
		
		// Check integration component versions
		if(_checkPluginVersions()) {
			_callIntegration(agent, command, docId);
		} else {
			inProgress = false;
		}
	}
	
	/**
	 * Parses a command received from the integration pipe
	 */
	function _parseIntegrationPipeCommand(string) {
		if(string != "") {
			// exec command if possible
			var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/);
			if(parts) {
				var agent = parts[1].toString();
				var cmd = parts[2].toString();
				var document = parts[3] ? parts[3].toString() : null;
				Zotero.Integration.execCommand(agent, cmd, document);
			} else {
				Components.utils.reportError("Zotero: Invalid integration input received: "+string);
			}
		}
	}
	
	/**
	 * Calls the Integration applicatoon
	 */
	function _callIntegration(agent, command, docId) {
		// Try to load the appropriate Zotero component; otherwise display an error using the alert
		// service
		try {
			var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1";
			Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : ""));
			var application = Components.classes[componentClass]
				.getService(Components.interfaces.zoteroIntegrationApplication);
		} catch(e) {
			_inProgress = false;
			Zotero.Integration.activate();
			Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
				.getService(Components.interfaces.nsIPromptService)
				.alert(null, Zotero.getString("integration.error.title"),
					Zotero.getString("integration.error.notInstalled"));
			throw e;
		}
		
		// Try to execute the command; otherwise display an error in alert service or word processor
		// (depending on what is possible)
		var integration, document;
		try {
			document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument());
			integration = new Zotero.Integration.Document(application, document);
			integration[command]();
			integration.cleanup();
		} catch(e) {
			if(integration) {
				try {
					integration.cleanup();
				} catch(e) {
					Components.utils.reportError(e);
				}
			}
			
			if(!(e instanceof Zotero.Integration.UserCancelledException)) {
				try {
					var displayError = null;
					if(e instanceof Zotero.Integration.DisplayException) {
						displayError = e.toString();
					} else {
						// check to see whether there's a pyxpcom error in the console, since it doesn't
						// get thrown directly
						var message = "";
						
						var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
							.getService(Components.interfaces.nsIConsoleService);
						
						var messages = {};
						consoleService.getMessageArray(messages, {});
						messages = messages.value;
						if(messages && messages.length) {
							var lastMessage = messages[messages.length-1];
							try {
								var error = lastMessage.QueryInterface(Components.interfaces.nsIScriptError);
							} catch(e2) {
								if(lastMessage.message && lastMessage.message.substr(0, 12) == "ERROR:xpcom:") {
									// print just the last line of the message, but re-throw the rest
									message = lastMessage.message.substr(0, lastMessage.message.length-1);
									message = "\n"+message.substr(message.lastIndexOf("\n"))
								}
							}
						}
						
						if(!message && typeof(e) == "object" && e.message) message = "\n\n"+e.message;
						
						if(message != "\n\nExceptionAlreadyDisplayed") {
							displayError = Zotero.getString("integration.error.generic")+message;
						}
						Zotero.debug(e);
					}
					
					if(displayError) {
						if(integration) {
							integration._doc.displayAlert(displayError,
									Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_STOP,
									Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK);
						} else {
							Zotero.Integration.activate();
							Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
								.getService(Components.interfaces.nsIPromptService)
								.alert(null, Zotero.getString("integration.error.title"), displayError);
						}
					}
				} finally {
					throw e;
				}
			}
		} finally {
			_inProgress = false;
		}
	}
	
	/**
	 * Activates Firefox
	 */
	this.activate = function(win) {
		if(Zotero.isMac) {
			const BUNDLE_IDS = {
				"Zotero":"org.zotero.zotero",
				"Firefox":"org.mozilla.firefox",
				"Minefield":"org.mozilla.minefield"
			};
			
			if(Zotero.isFx4 && win) {
				const carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon");
				/*
				 * struct ProcessSerialNumber {
				 *    unsigned long highLongOfPSN;
				 *    unsigned long lowLongOfPSN;
				 * };
				 */
				const ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", 
					[{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]);
					
				/*
				 * OSStatus SetFrontProcessWithOptions (
				 *    const ProcessSerialNumber *inProcess,
				 *    OptionBits inOptions
				 * );
				 */
				const SetFrontProcessWithOptions = carbon.declare("SetFrontProcessWithOptions",
					ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, ctypes.uint32_t);
				
				var psn = new ProcessSerialNumber();
				psn.highLongOfPSN = 0;
				psn.lowLongOfPSN = 2 // kCurrentProcess
				
				win.addEventListener("load", function() {
					var res = SetFrontProcessWithOptions(
						psn.address(),
						1 // kSetFrontProcessFrontWindowOnly = (1 << 0)
					);
					carbon.close();
				}, false);
			} else {
				if(Zotero.oscpu == "PPC Mac OS X 10.4" || Zotero.oscpu == "Intel Mac OS X 10.4"
				   || !BUNDLE_IDS[Zotero.appName]) {
					// 10.4 doesn't support "tell application id"
					_executeAppleScript('tell application "'+Zotero.appName+'" to activate');
				} else {
					_executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate');
				}
			}
		}
	}
	
	/**
	 * Runs an AppleScript on OS X
	 *
	 * @param script {String}
	 * @param block {Boolean} Whether the script should block until the process is finished.
	 */
	function _executeAppleScript(script, block) {
		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);
			try {
				proc.run(!!block, ['-e', script], 2);
			} catch(e) {}
		}
	}
}

/**
 * An exception thrown when a document contains an item that no longer exists in the current document.
 *
 * @param reselectKeys {Array} Keys representing the missing item
 * @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants)
 * @param citationIndex {Integer} The index of the missing item within the citation cluster
 * @param citationLength {Integer} The number of items cited in this citation cluster
 */
Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) {
	this.reselectKeys = reselectKeys;
	this.reselectKeyType = reselectKeyType;
	this.citationIndex = citationIndex;
	this.citationLength = citationLength;
}
Zotero.Integration.MissingItemException.prototype.name = "MissingItemException";
Zotero.Integration.MissingItemException.prototype.message = "An item in this document is missing from your Zotero library.";
Zotero.Integration.MissingItemException.prototype.toString = function() { return this.message; };

Zotero.Integration.UserCancelledException = function() {};
Zotero.Integration.UserCancelledException.prototype.name = "UserCancelledException";
Zotero.Integration.UserCancelledException.prototype.message = "User cancelled document update.";
Zotero.Integration.UserCancelledException.prototype.toString = function() { return this.message; };

Zotero.Integration.DisplayException = function(name, params) {
	this.name = name;
	this.params = params ? params : [];
};
Zotero.Integration.DisplayException.prototype.toString = function() { return Zotero.getString("integration.error."+this.name, this.params); };

Zotero.Integration.CorruptFieldException = function(corruptFieldString) {
	this.corruptFieldString = corruptFieldString;
}
Zotero.Integration.CorruptFieldException.prototype.name = "CorruptFieldException";
Zotero.Integration.CorruptFieldException.prototype.message = "A field code in this document is corrupted.";
Zotero.Integration.CorruptFieldException.prototype.toString = function() { return this.message+" "+this.corruptFieldString.toSource(); }

const INTEGRATION_TYPE_ITEM = 1;
const INTEGRATION_TYPE_BIBLIOGRAPHY = 2;
const INTEGRATION_TYPE_TEMP = 3;

// Placeholder for an empty bibliography
const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}";

/**
 * All methods for interacting with a document
 * @constructor
 */
Zotero.Integration.Document = function(app, doc) {
	this._app = app;
	this._doc = doc;
}

/**
 * Gets the type of the field
 */
Zotero.Integration.Document.prototype._getCodeTypeAndContent = function(rawCode) {
	for each(var code in ["ITEM", "CITATION"]) {
		if(rawCode.substr(0, code.length) === code) {
			return [INTEGRATION_TYPE_ITEM, rawCode.substr(code.length+1)];
		}
	}
	
	if(rawCode.substr(0, 4) === "BIBL") {
		return [INTEGRATION_TYPE_BIBLIOGRAPHY, rawCode.substr(5)];
	}
	
	if(rawCode.substr(0, 4) === "TEMP") {
		return [INTEGRATION_TYPE_TEMP, rawCode.substr(5)];
	}
	
	return [null, rawCode];
}

/**
 * Creates a new session
 * @param data {Zotero.Integration.DocumentData} Document data for new session
 */
Zotero.Integration.Document.prototype._createNewSession = function(data) {
	data.sessionID = Zotero.randomString();
	var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session();
	return session;
}

/**
 * Gets preferences for a document
 * @param require {Boolean} Whether an error should be thrown if no preferences or fields exist 
 *                          (otherwise, the set doc prefs dialog is shown)
 * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no
 *                                     preferences exist
 */
Zotero.Integration.Document.prototype._getSession = function(require, dontRunSetDocPrefs) {
	this._reloadSession = false;
	var dataString = this._doc.getDocumentData();
	if(!dataString) {
		var haveFields = false;
		var data = new Zotero.Integration.DocumentData();
		
		if(require) {
			// check to see if fields already exist
			for each(var fieldType in [this._app.primaryFieldType, this._app.secondaryFieldType]) {
				var fields = this._doc.getFields(this._app.primaryFieldType);
				if(fields.hasMoreElements()) {
					data.prefs.fieldType = this._app.primaryFieldType;
					haveFields = true;
					break;
				}
			}
			
			// if no fields, throw an error
			if(!haveFields) {
				throw new Zotero.Integration.DisplayException("mustInsertCitation");
			} else {
				Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields");
			}
		}
		
		// Set doc prefs if no data string yet
		this._session = this._createNewSession(data);
		this._session.setData(data);
		if(dontRunSetDocPrefs) return false;
		
		try {
			var ret = this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType);
		} finally {
			this._doc.activate();
		}
		
		// save doc prefs in doc
		this._doc.setDocumentData(this._session.data.serializeXML());
		
		if(haveFields) {
			this._reloadSession = true;
		}
	} else {
		var data = new Zotero.Integration.DocumentData(dataString);
		if(data.dataVersion < DATA_VERSION) {
			if(data.dataVersion == 1
					&& data.prefs.fieldType == "Field"
					&& this._app.primaryFieldType == "ReferenceMark") {
				// Converted OOo docs use ReferenceMarks, not fields
				data.prefs.fieldType = "ReferenceMark";
			}
			
			var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning"),
				Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_WARNING,
				Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL);
			if(!warning) throw new Zotero.Integration.UserCancelledException();
		} else if(data.dataVersion > DATA_VERSION) {
			throw new Zotero.Integration.DisplayException("newerDocumentVersion", [data.zoteroVersion, Zotero.version]);
		}
		
		if(data.prefs.fieldType !== this._app.primaryFieldType
				&& data.prefs.fieldType !== this._app.secondaryFieldType) {
			throw new Zotero.Integration.DisplayException("fieldTypeMismatch");
		}
		
		if(Zotero.Integration.sessions[data.sessionID]) {
			this._session = Zotero.Integration.sessions[data.sessionID];
		} else {
			this._session = this._createNewSession(data);
			try {
				this._session.setData(data);
			} catch(e) {
				// make sure style is defined
				if(e instanceof Zotero.Integration.DisplayException && e.name === "invalidStyle") {
					try {
						this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType);
					} finally {
						this._doc.activate();
					}
				} else {
					throw e;
				}
			}
			this._doc.setDocumentData(this._session.data.serializeXML());
			
			this._reloadSession = true;
		}
	}
	
	this._session.resetRequest(this);
	return !!dataString;
}

/**
 * Gets all fields for a document
 * @param require {Boolean} Whether an error should be thrown if no fields exist
 * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences window if no
 *                                     preferences exist
 */
Zotero.Integration.Document.prototype._getFields = function(require, dontRunSetDocPrefs) {
	if(this._fields) return;
	if(!this._session && !this._getSession(require, dontRunSetDocPrefs)) return;
	
	var getFieldsTime = (new Date()).getTime();
	var fields = this._doc.getFields(this._session.data.prefs['fieldType']);
	this._fields = [];
	while(fields.hasMoreElements()) {
		this._fields.push(fields.getNext().QueryInterface(Components.interfaces.zoteroIntegrationField));
	}
	var endTime = (new Date()).getTime();
	if(Zotero.Debug.enabled) {
		Zotero.debug("Integration: got "+this._fields.length+" fields in "+
			(endTime-getFieldsTime)/1000+"; "+
			1000/((endTime-getFieldsTime)/this._fields.length)+" fields/second");
	}
	
	if(require && !this._fields.length) {
		throw new Zotero.Integration.DisplayException("mustInsertCitation");
	}
	
	return;
}

/**
 * Checks that it is appropriate to add fields to the current document at the current
 * positon, then adds one.
 */
Zotero.Integration.Document.prototype._addField = function(note) {
	// Get citation types if necessary
	if(!this._doc.canInsertField(this._session.data.prefs['fieldType'])) {
		throw new Zotero.Integration.DisplayException("cannotInsertHere");
		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;
}

/**
 * Shows an error if a field code is corrupted
 * @param {Exception} e The exception thrown
 * @param {Field} field The Zotero field object
 * @param {Integer} i The field index
 */
Zotero.Integration.Document.prototype._showCorruptFieldError = function(e, field, i) {
	var msg = Zotero.getString("integration.corruptField")+'\n\n'+
			  Zotero.getString('integration.corruptField.description');
	field.select();
	var result = this._doc.displayAlert(msg,
		Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
		Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO_CANCEL);
	
	if(result == 0) {
		throw e;
	} else if(result == 1) {		// No
		this._removeCodeFields.push(i);
	} else {
		// Display reselect edit citation dialog
		var added = this._session.editCitation(i, field.getNoteIndex());
		if(added) {
			this._doc.activate();
		} else {
			throw new Zotero.Integration.UserCancelledException();
		}
	}
}

/**
 * Loads existing citations and bibliographies out of a document, and creates or edits fields
 */
Zotero.Integration.Document.prototype._updateSession = function(newField, editField) {
	var deleteKeys = {};
	this._deleteFields = [];
	this._removeCodeFields = [];
	this._bibliographyFields = [];
	var bibliographyData = "";
	
	// first collect entire bibliography
	this._getFields();
	var editFieldIndex = false;
	var collectFieldsTime = (new Date()).getTime();
	for(var i in this._fields) {
		var field = this._fields[i];
		
		if(editField && field.equals(editField)) {
			editFieldIndex = i;
		} else {
			try {
				var fieldCode = field.getCode();
			} catch(e) {
				this._showCorruptFieldError(e, field, i);
			}
			
			var [type, content] = this._getCodeTypeAndContent(fieldCode);
			if(type === INTEGRATION_TYPE_ITEM) {
				var noteIndex = (this._session.styleClass == "note" ? field.getNoteIndex() : 0);
				try {
					this._session.addCitation(i, noteIndex, content);
				} 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+1).toString());
							}
							msg += '\n\n'+Zotero.getString('integration.missingItem.description');
							field.select();
							var result = this._doc.displayAlert(msg, 1, 3);
							if(result == 0) {			// Cancel
								throw new Zotero.Integration.UserCancelledException();
							} 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
								this._session.reselectItem(e);
								// Now try again
								this._session.addCitation(i, field.getNoteIndex(), content);
								this._doc.activate();
							}
						}
					} else if(e instanceof Zotero.Integration.CorruptFieldException) {
						this._showCorruptFieldError(e, field, i);
					} else {
						throw e;
					}
				}
			} else if(type === INTEGRATION_TYPE_BIBLIOGRAPHY) {
				this._bibliographyFields.push(field);
				if(!this._session.bibliographyData && !bibliographyData) {
					bibliographyData = content;
				}
			} else if(type === INTEGRATION_TYPE_TEMP) {
				if(newField && newField.equals(field)) {
					editFieldIndex = i;
					editField = field;
				} else {
					this._deleteFields.push(i);
				}
			}
		}
	}
	var endTime = (new Date()).getTime();
	if(Zotero.Debug.enabled) {
		Zotero.debug("Integration: collected "+this._fields.length+" fields in "+
			(endTime-collectFieldsTime)/1000+"; "+
			1000/((endTime-collectFieldsTime)/this._fields.length)+" fields/second");
	}
	
	// load uncited items from bibliography
	if(bibliographyData && !this._session.bibliographyData) {
		try {
			this._session.loadBibliographyData(bibliographyData);
		} catch(e) {
			if(e instanceof Zotero.Integration.CorruptFieldException) {
				var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+
						  Zotero.getString('integration.corruptBibliography.description');
				var result = this._doc.displayAlert(msg, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_OK_CANCEL);
				if(result == 0) {
					throw e;
				} else {
					bibliographyData = "";
					this._session.bibliographyHasChanged = true;
					this._session.bibliographyDataHasChanged = true;
				}
			} else {
				throw e;
			}
		}
	}
	
	// if we are reloading this session, assume no item IDs to be updated except for edited items
	if(this._reloadSession) {
		//this._session.restoreProcessorState(); TODO doesn't appear to be working properly
		this._session.updateUpdateIndices();
		var deleteCitations = this._session.updateCitations();
		this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]);
		this._session.updateIndices = {};
		this._session.updateItemIDs = {};
		this._session.bibliographyHasChanged = false;
	}
	
	// create new citation or edit existing citation
	if(editFieldIndex) { 
		var [type, editFieldCode] = this._getCodeTypeAndContent(editField.getCode());
		var editCitation = editFieldCode ? this._session.unserializeCitation(editFieldCode, editFieldIndex) : null;
		
		var editNoteIndex = editField.getNoteIndex();
		var added = this._session.editCitation(editFieldIndex, editNoteIndex, editCitation);
		this._doc.activate();
		
		if(!added) {
			if(editFieldCode) {	// cancelled editing; just add as if nothing happened
				this._session.addCitation(editFieldIndex, editNoteIndex, editCitation);
			} else {			// cancelled creation; delete the citation
				this._session.deleteCitation(editFieldIndex);
			}
		}
	}
}

/**
 * Updates bibliographies and fields within a document
 * @param {Boolean} forceCitations Whether to regenerate all citations
 * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries
 * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been 
 *	modified since they were created, instead of showing a warning
 */
Zotero.Integration.Document.prototype._updateDocument = function(forceCitations, forceBibliography,
		ignoreCitationChanges) {
	// update citations
	this._session.updateUpdateIndices(forceCitations);
	var deleteCitations = this._session.updateCitations();
	this._deleteFields = this._deleteFields.concat([i for(i in deleteCitations)]);
	for(var i in this._session.updateIndices) {
		var citation = this._session.citationsByIndex[i];
		var field = this._fields[i];
		
		// If there is no citation, we're deleting it, or we shouldn't update it, ignore it
		if(!citation || deleteCitations[i]) continue;
		
		if(!citation.properties.dontUpdate) {
			var isRich = false;
			var formattedCitation = citation.properties.custom
				? citation.properties.custom : this._session.citationText[i];
			
			if(formattedCitation.indexOf("\\") !== -1) {
				// need to set text as RTF
				formattedCitation = "{\\rtf "+formattedCitation+"}"
				isRich = true;
			}
			
			if(forceCitations || citation.properties.formattedCitation !== formattedCitation) {
				// Check if citation has been manually modified
				if(!ignoreCitationChanges && citation.properties.plainCitation) {
					var plainCitation = field.getText();
					if(plainCitation !== citation.properties.plainCitation) {
						// Citation manually modified; ask user if they want to save changes
						field.select();
						var result = this._doc.displayAlert(
							Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_ICON_CAUTION, 
							Components.interfaces.zoteroIntegrationDocument.DIALOG_BUTTONS_YES_NO);
						if(result) {
							citation.properties.dontUpdate = true;
						}
					}
				}
				
				if(!citation.properties.dontUpdate) {
					field.setText(formattedCitation, isRich);
					
					citation.properties.formattedCitation = formattedCitation;
					citation.properties.plainCitation = field.getText();
				}
			}
		}
		
		var fieldCode = this._session.getCitationField(citation);
		if(fieldCode != citation.properties.field) {
			field.setCode(
				(this._session.data.prefs.storeReferences ? "ITEM CSL_CITATION" : "ITEM")
				+" "+fieldCode);
			
			if(this._session.data.prefs.fieldType === "ReferenceMark" && isRich
					&& !citation.properties.dontUpdate) {
				// For ReferenceMarks with formatting, we need to set the text again, because
				// setting the field code removes formatting from the mark. I don't like this.
				field.setText(formattedCitation, isRich);
			}
		}
	}
	
	// update bibliographies
	if(this._bibliographyFields.length	 				// if blbliography exists
			&& (this._session.bibliographyHasChanged	// and bibliography changed
			|| forceBibliography)) {					// or if we should generate regardless of changes
		if(forceBibliography || this._session.bibliographyDataHasChanged) {
			var bibliographyData = this._session.getBibliographyData();
			for each(var field in this._bibliographyFields) {
				field.setCode("BIBL "+bibliographyData
					+(this._session.data.prefs.storeReferences ? " CSL_BIBLIOGRAPHY" : ""));
			}
		}
		
		// get bibliography and format as RTF
		var bib = this._session.getBibliography();
		
		var bibliographyText = "";
		if(bib) {
			bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend;
			
			// if bibliography style not set, set it
			if(!this._session.data.style.bibliographyStyleHasBeenSet) {
				var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib);
				
{'fraction_non_alphanumeric': 0.12048287092127513, 'fraction_numerical': 0.003914323291789316, 'mean_word_length': 3.69486915613055, 'pattern_counts': {'":': 12, '<': 17, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1776933', 'n_tokens_mistral': 23611, 'n_tokens_neox': 19383, 'n_words': 4182}