zotero/chrome/content/zotero/xpcom/integration.js

1550 lines
46 KiB
JavaScript

/*
***** 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.s
***** END LICENSE BLOCK *****
*/
const API_VERSION = 1;
const COMPAT_API_VERSION = 5;
Zotero.Integration = 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.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();
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);
// ugh: we can't use real SOAP, since AppleScript VBA can't pass
// objects, so implode arrays
if(!output) {
output = "";
}
if(typeof(output) == "object") {
for(var i in output) {
if(typeof(output[i]) == "string") {
output[i] = output[i].replace(/:/g, "::");
}
}
output = output.join(":");
}
// create envelope
var responseEnvelope = <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);
// return OK
return _generateResponse("200 OK", 'text/xml; charset="UTF-8"',
response);
} else {
Zotero.debug("Integration: SOAP method not supported");
}
} else {
// execute request
request = new Zotero.Integration.Request(xml);
return _generateResponse(request.status+" "+request.statusText,
'text/xml; charset="UTF-8"', request.responseText);
}
}
/*
* 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.init();
}
}
};
var observerService =
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
observerService.addObserver(observer, "network:offline-status-changed", false);
_onlineObserverRegistered = true;
}
}
Zotero.Integration.SocketListener = new function() {
this.onSocketAccepted = onSocketAccepted;
this.onStopListening = onStopListening;
/*
* called when a socket is opened
*/
function onSocketAccepted(socket, transport) {
// get an input stream
var iStream = transport.openInputStream(0, 0, 0);
var oStream = transport.openOutputStream(0, 0, 0);
var dataListener = new Zotero.Integration.DataListener(iStream, oStream);
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]
.createInstance(Components.interfaces.nsIInputStreamPump);
pump.init(iStream, -1, -1, 0, 0, false);
pump.asyncRead(dataListener, null);
}
function onStopListening(serverSocket, status) {
Zotero.debug("Integration HTTP server going offline");
}
}
/*
* handles the actual acquisition of data
*/
Zotero.Integration.DataListener = function(iStream, oStream) {
this.header = "";
this.headerFinished = false;
this.body = "";
this.bodyLength = 0;
this.iStream = iStream;
this.oStream = oStream;
this.sStream = Components.classes["@mozilla.org/scriptableinputstream;1"]
.createInstance(Components.interfaces.nsIScriptableInputStream);
this.sStream.init(iStream);
this.foundReturn = false;
}
/*
* called when a request begins (although the request should have begun before
* the DataListener was generated)
*/
Zotero.Integration.DataListener.prototype.onStartRequest = function(request, context) {}
/*
* called when a request stops
*/
Zotero.Integration.DataListener.prototype.onStopRequest = function(request, context, status) {
this.iStream.close();
this.oStream.close();
}
/*
* called when new data is available
*/
Zotero.Integration.DataListener.prototype.onDataAvailable = function(request, context,
inputStream, offset, count) {
var readData = this.sStream.read(count);
if(this.headerFinished) { // reading body
this.body += readData;
// check to see if data is done
this._bodyData();
} else { // reading header
// see if there's a magic double return
var lineBreakIndex = readData.indexOf("\r\n\r\n");
if(lineBreakIndex != -1) {
if(lineBreakIndex != 0) {
this.header += readData.substr(0, lineBreakIndex+4);
this.body = readData.substr(lineBreakIndex+4);
}
this._headerFinished();
return;
}
var lineBreakIndex = readData.indexOf("\n\n");
if(lineBreakIndex != -1) {
if(lineBreakIndex != 0) {
this.header += readData.substr(0, lineBreakIndex+2);
this.body = readData.substr(lineBreakIndex+2);
}
this._headerFinished();
return;
}
if(this.header && this.header[this.header.length-1] == "\n" &&
(readData[0] == "\n" || readData[0] == "\r")) {
if(readData.length > 1 && readData[1] == "\n") {
this.header += readData.substr(0, 2);
this.body = readData.substr(2);
} else {
this.header += readData[0];
this.body = readData.substr(1);
}
this._headerFinished();
return;
}
this.header += readData;
}
}
/*
* processes an HTTP header and decides what to do
*/
Zotero.Integration.DataListener.prototype._headerFinished = function() {
this.headerFinished = true;
var output = Zotero.Integration.handleHeader(this.header);
if(typeof(output) == "number") {
this.bodyLength = output;
// check to see if data is done
this._bodyData();
} else {
this._requestFinished(output);
}
}
/*
* checks to see if Content-Length bytes of body have been read and, if they
* have, processes the body
*/
Zotero.Integration.DataListener.prototype._bodyData = function() {
if(this.body.length >= this.bodyLength) {
// convert to UTF-8
var dataStream = Components.classes["@mozilla.org/io/string-input-stream;1"]
.createInstance(Components.interfaces.nsIStringInputStream);
dataStream.setData(this.body, this.bodyLength);
var utf8Stream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
.createInstance(Components.interfaces.nsIConverterInputStream);
utf8Stream.init(dataStream, "UTF-8", 4096, "?");
this.body = "";
var string = {};
while(utf8Stream.readString(this.bodyLength, string)) {
this.body += string.value;
}
// handle envelope
var output = Zotero.Integration.handleEnvelope(this.body);
this._requestFinished(output);
}
}
/*
* returns HTTP data from a request
*/
Zotero.Integration.DataListener.prototype._requestFinished = function(response) {
// close input stream
this.iStream.close();
// open UTF-8 converter for output stream
var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Components.interfaces.nsIConverterOutputStream);
// write
try {
intlStream.init(this.oStream, "UTF-8", 1024, "?".charCodeAt(0));
// write response
intlStream.writeString(response);
} catch(e) {
Zotero.debug("An error occurred.");
Zotero.debug(e);
} finally {
intlStream.close();
}
}
Zotero.Integration.Request = function(xml) {
var env = Zotero.Integration.env;
this.header = xml.env::Header;
this.body = xml.env::Body;
this.responseXML = <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;
this.needPrefs = this.body.setDocPrefs.length();
try {
this.initializeSession();
if(this.needPrefs) {
this.setDocPrefs();
}
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) {
}
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>
this.status = 500;
this.statusText = "Internal Server Error";
}
// 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({});
if(this.header.client.@api != API_VERSION) {
throw "incompatibleVersion";
}
var styleID = this.header.style.@id.toString();
this._sessionID = this.header.session.@id.toString();
if(this._sessionID === "" || !Zotero.Integration.sessions[this._sessionID]) {
this._sessionID = Zotero.randomString();
this._session = Zotero.Integration.sessions[this._sessionID] = new Zotero.Integration.Session();
var preferences = {};
for each(var pref in this.header.prefs.pref) {
preferences[pref.@name] = pref.@value.toString();
}
this.needPrefs = this.needPrefs || !this._session.setStyle(styleID, preferences);
} else {
this._session = Zotero.Integration.sessions[this._sessionID];
}
this.responseHeader.appendChild(<session id={this._sessionID}/>);
}
/**
* Sets preferences
**/
Zotero.Integration.Request.prototype.setDocPrefs = function() {
default xml namespace = Zotero.Integration.ns; with({});
var io = new function() {
this.wrappedJSObject = this;
}
io.openOffice = this.header.client.@agent == "OpenOffice.org"
var oldStyle = io.style = this._session.styleID;
io.useEndnotes = this._session.prefs.useEndnotes;
io.useBookmarks = this._session.prefs.fieldType;
this.watcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher)
.openWindow(null, 'chrome://zotero/content/integrationDocPrefs.xul', '',
'chrome,modal,centerscreen' + (Zotero.isWin ? ',popup' : ''), io, true);
if(!oldStyle || oldStyle != io.style
|| io.useEndnotes != this._session.prefs.useEndnotes
|| io.useBookmarks != this._session.prefs.fieldType) {
this._session.regenerateAll = this._session.bibliographyHasChanged = true;
if(oldStyle != io.style) {
this._session.setStyle(io.style, this._session.prefs);
}
}
this._session.prefs.useEndnotes = io.useEndnotes;
this._session.prefs.fieldType = io.useBookmarks;
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/>);
}
/**
* 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);
}
}
// load bibliography data here
if(this.header.bibliography.length()) {
this._session.loadBibliographyData(Zotero.Utilities.prototype.trim(this.header.bibliography.toString()));
}
this._session.updateItemSet();
// 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();
}
Zotero.Integration.SOAP_Compat = new function() {
// SOAP methods
this.update = update;
this.restoreSession = restoreSession;
this.setDocPrefs = setDocPrefs;
/*
* 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];
}
// 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]);
}
} 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);
}
}
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("!");
}
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.isWin ? ',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;
}
}
/*
* 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.resetRequest();
}
/*
* changes the Session style
*/
Zotero.Integration.Session.prototype.setStyle = function(styleID, prefs) {
this.prefs = prefs;
if(styleID) {
this.styleID = styleID;
try {
this.style = Zotero.Styles.get(styleID).csl;
this.dateModified = new Object();
this.itemSet = this.style.createItemSet();
this.loadUncitedItems();
} catch(e) {
Zotero.debug(e)
this.styleID = undefined;
return false;
}
return true;
}
return false;
}
/*
* resets per-request variables in the CitationSet
*/
Zotero.Integration.Session.prototype.resetRequest = function() {
this.citationsByItemID = new Object();
this.citationsByIndex = new Array();
this.regenerateAll = false;
this.bibliographyHasChanged = false;
this.bibliographyDataHasChanged = false;
this.updateItemIDs = new Object();
this.updateIndices = new Object()
}
/*
* generates a field from a citation object
*/
Zotero.Integration.Session._acceptableTypes = ["string", "boolean", "number"];
Zotero.Integration.Session._saveProperties = ["custom"];
Zotero.Integration.Session.prototype.getCitationField = function(citation) {
var type, field = "";
for(var j=0; j<Zotero.Integration.Session._saveProperties.length; j++) {
var property = Zotero.Integration.Session._saveProperties[j];
if(citation.properties[property]) {
field += ',"'+property+'":'+Zotero.JSON.serialize(citation.properties[property]);
}
}
var citationItems = "";
for(var j=0; j<citation.citationItems.length; j++) {
var citationItem = "";
// ensure key is saved
if(citation.citationItems[j].key == undefined) {
citation.citationItems[j].key = citation.citationItems[j].item.key;
}
for(var k in citation.citationItems[j]) {
type = typeof(citation.citationItems[j][k]);
if(citation.citationItems[j][k] && k != "itemID" && Zotero.Integration.Session._acceptableTypes.indexOf(type) !== -1) {
citationItem += ',"'+k+'":'+Zotero.JSON.serialize(citation.citationItems[j][k]);
}
}
citationItems += ",{"+citationItem.substr(1)+"}";
}
field += ',"citationItems":['+citationItems.substr(1)+"]";
return "{"+field.substr(1)+"}";
}
/*
* adds a citation based on a serialized Word field
*/
Zotero.Integration._oldCitationLocatorMap = {
p:Zotero.CSL.LOCATOR_PAGES,
g:Zotero.CSL.LOCATOR_PARAGRAPH,
l:Zotero.CSL.LOCATOR_LINE
};
/*
* gets a Zotero.CSL.Citation object given a field name
*/
Zotero.Integration.Session.prototype.addCitation = function(index, arg) {
var index = parseInt(index, 10);
if(typeof(arg) == "string") { // text field
if(arg == "!" || arg == "X") return;
var citation = this.unserializeCitation(arg, index);
} else { // a citation already
var citation = arg;
}
var completed = this.completeCitation(citation);
if(!completed) {
// doesn't exist
this.deleteCitation(index);
return;
}
// add to citationsByItemID and citationsByIndex
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 {
// 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);
}
}
}
citation.properties.index = index;
this.citationsByIndex[index] = citation;
}
/*
* adds items to a citation whose citationItems contain only item IDs
*/
Zotero.Integration.Session.prototype.completeCitation = function(object) {
// replace item IDs with real items
for(var i=0; i<object.citationItems.length; i++) {
var citationItem = object.citationItems[i];
var zoteroItem;
if(citationItem.key) {
var item = this.itemSet.getItemsByKeys([citationItem.key])[0];
} else {
this.updateItemIDs[citationItem.itemID] = true;
var item = this.itemSet.getItemsByIds([citationItem.itemID])[0];
}
// loop through items not in itemSet
if(item == false) {
if(citationItem.key) {
zoteroItem = Zotero.Items.getByKey(citationItem.key);
} else {
zoteroItem = Zotero.Items.get(citationItem.itemID);
}
if(!zoteroItem) return false;
item = this.itemSet.add([zoteroItem])[0];
this.dateModified[citationItem.itemID] = item.zoteroItem.getField("dateModified", true, true);
this.updateItemIDs[citationItem.itemID] = true;
this.bibliographyHasChanged = true;
}
citationItem.item = item;
if(!citationItem.itemID) citationItem.itemID = item.id;
}
return true;
}
/*
* unserializes a JSON citation into a citation object (sans items)
*/
Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) {
if(arg[0] == "{") { // JSON field
// create citation
var citation = this.style.createCitation();
// fix for corrupted fields
var lastBracket = arg.lastIndexOf("}");
if(lastBracket+1 != arg.length) {
arg = arg.substr(0, lastBracket+1);
this.updateIndices[index] = true;
} else {
citation.properties.field = arg;
}
// get JSON
var object = Zotero.JSON.unserialize(arg);
// Fix uppercase citation codes
if(object.CITATIONITEMS) {
object.citationItems = [];
for (var i=0; i<object.CITATIONITEMS.length; i++) {
for (var j in object.CITATIONITEMS[i]) {
switch (j) {
case 'ITEMID':
var field = 'itemID';
break;
// 'position', 'custom'
default:
var field = j.toLowerCase();
}
if (!object.citationItems[i]) {
object.citationItems[i] = {};
}
object.citationItems[i][field] = object.CITATIONITEMS[i][j];
}
}
}
// copy properties
for(var i in object) {
if(Zotero.Integration.Session._saveProperties.indexOf(i) != -1) {
citation.properties[i] = object[i];
} else {
citation[i] = object[i];
}
}
} else { // ye olde style field
var underscoreIndex = arg.indexOf("_");
var itemIDs = arg.substr(0, underscoreIndex).split("|");
var lastIndex = arg.lastIndexOf("_");
if(lastIndex != underscoreIndex+1) {
var locatorString = arg.substr(underscoreIndex+1, lastIndex-underscoreIndex-1);
var locators = locatorString.split("|");
}
var citationItems = new Array();
for(var i=0; i<itemIDs.length; i++) {
var citationItem = {itemID:itemIDs[i]};
if(locators) {
citationItem.locator = locators[i].substr(1);
citationItem.locatorType = Zotero.Integration._oldCitationLocatorMap[locators[i][0]];
}
citationItems.push(citationItem);
}
var citation = this.style.createCitation(citationItems);
this.updateIndices[index] = true;
}
return citation;
}
/*
* marks 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
* and position) and an index
*/
Zotero.Integration.Session.prototype.previewCitation = function(citation) {
// get length of item set, so we can tell how many items we've added
var itemSetLength = this.itemSet.items.length;
// add citation items
this.completeCitation(citation);
// get list of items we later have to delete
var deleteItems = this.itemSet.items.slice(itemSetLength, this.itemSet.items.length);
// get position
this.getCitationPositions(citation);
// sort item set
this.sortItemSet();
// sort citation if desired
if(citation.properties.sort) {
citation.sort();
}
// get preview citation
var text = this.style.formatCitation(citation, "Integration");
// delete from item set
if(deleteItems.length) {
this.itemSet.remove(deleteItems);
}
return text;
}
/*
* brings up the addCitationDialog, prepopulated if a citation is provided
*/
Zotero.Integration.Session.prototype.editCitation = function(index, citation) {
var me = this;
var io = new function() { this.wrappedJSObject = this; }
// if there's already a citation, make sure we have item IDs in addition to keys
if(citation) {
for each(var citationItem in citation.citationItems) {
if(citationItem.key && !citationItem.itemID) {
var item = Zotero.Items.getByKey([citationItem.key]);
if(item) citationItem.itemID = item.itemID;
}
}
}
// create object to hold citation
io.citation = (citation ? citation.clone() : this.style.createCitation());
io.citation.properties.index = parseInt(index, 10);
// assign preview function
io.previewFunction = function() {
return me.previewCitation(io.citation);
}
Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher)
.openWindow(null, 'chrome://zotero/content/addCitationDialog.xul', '',
'chrome,modal,centerscreen,resizable=yes' + (Zotero.isWin ? ',popup' : ''), io);
if(citation && !io.citation.citationItems.length) {
io.citation = citation;
}
if(io.citation.citationItems.length) { // we have an item
this.addCitation(index, io.citation);
this.updateIndices[index] = true;
}
// resort item set if necessary
this.sortItemSet();
return !!io.citation.citationItems.length;
}
/*
* sets position attribute on a citation
*/
Zotero.Integration.Session.prototype.getCitationPositions = function(citation, update) {
for(var previousIndex = citation.properties.index-1;
previousIndex != -1
&& (!this.citationsByIndex[previousIndex]
|| this.citationsByIndex[previousIndex].properties.delete);
previousIndex--) {}
var previousCitation = (previousIndex == -1 ? false : this.citationsByIndex[previousIndex]);
// if only one source, and it's the same as the last, use ibid
if( // there must be a previous citation with one item, and this citation
// may only have one item
previousCitation && citation.citationItems.length == 1
&& previousCitation.citationItems.length == 1
// the previous citation must have been a citation of the same item
&& citation.citationItems[0].item == previousCitation.citationItems[0].item
// and if the previous citation had a locator (page number, etc.)
// then this citation must have a locator, or else we should do the
// full citation (see Chicago Manual of Style)
&& (!previousCitation.citationItems[0].locator || citation.citationItems[0].locator)) {
// use ibid, but check whether to use ibid+pages
var newPosition = (citation.citationItems[0].locator == previousCitation.citationItems[0].locator
&& citation.citationItems[0].locatorType == previousCitation.citationItems[0].locatorType
? Zotero.CSL.POSITION_IBID : Zotero.CSL.POSITION_IBID_WITH_LOCATOR);
// update if desired
if(update && (citation.citationItems[0].position || newPosition) && citation.citationItems[0].position != newPosition) {
this.updateIndices[citation.properties.index] = true;
}
citation.citationItems[0].position = newPosition;
} else {
// loop through to see which are first citations
for(var i=0; i<citation.citationItems.length; i++) {
var citationItem = citation.citationItems[i];
var newPosition = (!this.citationsByItemID[citationItem.itemID]
|| this.citationsByItemID[citationItem.itemID][0].properties.index >= citation.properties.index
? Zotero.CSL.POSITION_FIRST : Zotero.CSL.POSITION_SUBSEQUENT);
// update if desired
if(update && (citation.citationItems[i].position || newPosition) && citation.citationItems[i].position != newPosition) {
this.updateIndices[citation.properties.index] = true;
}
citation.citationItems[i].position = newPosition;
}
}
}
/*
* marks citations for update, where necessary
*/
Zotero.Integration.Session.prototype.updateCitations = function(toIndex) {
if(!toIndex) toIndex = this.citationsByIndex.length-1;
for(var i=0; i<=toIndex; i++) {
var citation = this.citationsByIndex[i];
// get position, updating if necesary
if(citation && !citation.properties.delete && !citation.properties.custom) {
this.getCitationPositions(citation, true);
}
}
}
/*
* updates the ItemSet, adding and deleting bibliography items as appropriate,
* then re-sorting
*/
Zotero.Integration.Session.prototype.updateItemSet = function() {
var addItems = [];
var deleteItems = [];
for(var i in this.citationsByItemID) {
// see if items were deleted from Zotero
if (!Zotero.Items.get(i)) {
deleteItems.push(itemID);
if(this.citationsByItemID[i].length) {
for(var j=0; j<this.citationsByItemID[i].length; j++) {
var citation = this.citationsByItemID[i][j];
this.updateIndices[citation.properties.index] = true;
citation.properties.delete = true;
}
}
}
}
// see if old items were deleted or changed
for each(var item in this.itemSet.items) {
var itemID = item.id;
// see if items were removed
if(!this.citationsByItemID[itemID] && !this.uncitedItems[itemID]) {
deleteItems.push(itemID);
continue;
}
if(item.zoteroItem && this.dateModified[itemID] != item.zoteroItem.getField("dateModified", true, true)) {
// update date modified
this.dateModified[itemID] = item.zoteroItem.getField("dateModified", true, true);
// add to list of updated item IDs
this.updateItemIDs[itemID] = true;
}
}
if(deleteItems.length) {
this.itemSet.remove(deleteItems);
this.bibliographyHasChanged = true;
}
this.sortItemSet();
}
/*
* sorts the ItemSet (what did you think it did?)
*/
Zotero.Integration.Session.prototype.sortItemSet = function() {
// save first index
for(var itemID in this.citationsByItemID) {
if(this.citationsByItemID[itemID]) {
var item = this.itemSet.getItemsByIds([itemID])[0];
if(item) item.setProperty("index", this.citationsByItemID[itemID][0].properties.index);
}
}
var citationChanged = this.itemSet.resort();
// add to list of updated item IDs
for each(var item in citationChanged) {
this.updateItemIDs[item.id] = true;
this.bibliographyHasChanged = true;
}
}
/*
* edits integration bibliography
*/
Zotero.Integration.Session.prototype.editBibliography = function() {
var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this);
var io = new function() { this.wrappedJSObject = bibliographyEditor; }
this.bibliographyDataHasChanged = this.bibliographyHasChanged = true;
Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher)
.openWindow(null, 'chrome://zotero/content/editBibliographyDialog.xul', '',
'chrome,modal,centerscreen,resizable=yes' + (Zotero.isWin ? ',popup' : ''), io, true);
}
/*
* 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 "";
}
} else {
return this.style.formatBibliography(this.itemSet, "Integration");
}
}
/*
* gets citations in need of update
*/
Zotero.Integration.Session.prototype.getCitations = function(regenerateAll, useXML) {
if(regenerateAll || this.regenerateAll) {
// update all indices
for(var i=0; i<this.citationsByIndex.length; i++) {
this.updateIndices[i] = true;
}
} else {
// update only item IDs
for(var i in this.updateItemIDs) {
if(this.citationsByItemID[i] && this.citationsByItemID[i].length) {
for(var j=0; j<this.citationsByItemID[i].length; j++) {
this.updateIndices[this.citationsByItemID[i][j].properties.index] = true;
}
}
}
}
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"]) {
// delete citation
if(useXML) {
citationXML.@delete = "1";
} else {
output.push("!");
output.push("!");
}
} else {
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
*/
Zotero.Integration.Session.prototype.loadBibliographyData = function(json) {
var documentData = Zotero.JSON.unserialize(json);
// set uncited
if(documentData.uncited) {
this.uncitedItems = documentData.uncited;
this.loadUncitedItems();
} else {
this.uncitedItems = new Object();
}
// set custom bibliography entries
if(documentData.custom) {
for(var itemID in documentData.custom) {
if(typeof(itemID) == "string") { // key
var item = this.itemSet.getItemsByKeys([itemID])[0];
} else { // item ID
this.bibliographyDataHasChanged = true;
var item = this.itemSet.getItemsByIds([itemID])[0];
}
if (!item) {
continue;
}
item.setProperty("bibliography-RTF", documentData.custom[itemID]);
}
}
}
/*
* adds items in this.uncitedItems to itemSet, if they are not already there
*/
Zotero.Integration.Session.prototype.loadUncitedItems = function() {
var needConversion = false;
for(var itemID in this.uncitedItems) {
// skip "undefined"
if(!this.uncitedItems[itemID]) continue;
// if not yet in item set, add to item set
if(typeof(itemID) == "string") { // key
var item = this.itemSet.getItemsByKeys([itemID])[0];
itemID = Zotero.Items.getByKey(itemID);
} else { // item ID
needConversion = true;
var item = this.itemSet.getItemsByIds([itemID])[0];
}
if(!item) this.itemSet.add([itemID])[0];
}
// need a second loop to convert, since we need to modify this.uncitedItems
if(needConversion) {
this.bibliographyDataHasChanged = true;
oldUncitedItems = this.uncitedItems;
this.uncitedItems = {};
for(var itemID in oldUncitedItems) {
if(!oldUncitedItems[itemID]) continue;
if(typeof(itemID) == "string") { // key
this.uncitedItems[itemID] = true;
} else { // itemID
var item = Zotero.Items.get(itemID);
if(item) {
this.uncitedItems[item.key] = true;
}
}
}
}
}
/*
* saves document data from a JSON object
*/
Zotero.Integration.Session.prototype.getBibliographyData = function() {
var bibliographyData = {};
// add uncited if there is anything
for each(var item in this.uncitedItems) {
if(item) {
bibliographyData.uncited = this.uncitedItems;
break;
}
}
// look for custom bibliography entries
if(this.itemSet.items.length) {
for(var i=0; i<this.itemSet.items.length; i++) {
var custom = this.itemSet.items[i].getProperty("bibliography-Integration");
if(custom !== "") {
if(!bibliographyData.custom) bibliographyData.custom = {};
bibliographyData.custom[this.itemSet.items[i].key] = custom;
}
}
}
if(bibliographyData.uncited || bibliographyData.custom) {
return Zotero.JSON.serialize(bibliographyData);
} else {
return ""; // nothing
}
}
/**
* @class Interface for bibliography editor to alter document bibliography
* @constructor
* Creates a new bibliography editor interface
* @param {Zotero.Integration.Session} session
*/
Zotero.Integration.Session.BibliographyEditInterface = function(session) {
this.session = session;
}
/**
* Gets the @link {Zotero.CSL.ItemSet} for the bibliography being edited
* The item set should not be modified, but may be used to determine what items are in the
* bibliography.
*/
Zotero.Integration.Session.BibliographyEditInterface.prototype.getItemSet = function() {
return this.session.itemSet;
}
/**
* Checks whether an item is cited in the bibliography being edited
*/
Zotero.Integration.Session.BibliographyEditInterface.prototype.isCited = function(item) {
if(this.session.citationsByItemID[item.id]) return true;
return false;
}
/**
* Checks whether an item is cited in the bibliography being edited
*/
Zotero.Integration.Session.BibliographyEditInterface.prototype.add = function(item) {
// create new item
this.session.itemSet.add([item]);
this.session.uncitedItems[item.key] = true;
this.session.sortItemSet();
}
/**
* Removes an item from the bibliography being edited
*/
Zotero.Integration.Session.BibliographyEditInterface.prototype.remove = function(item) {
// create new item
this.session.itemSet.remove([item]);
this.session.sortItemSet();
// delete citations if necessary
var itemID = item.id;
if(this.session.citationsByItemID[itemID]) {
for(var j=0; j<this.session.citationsByItemID[itemID].length; j++) {
var citation = this.session.citationsByItemID[itemID][j];
this.session.updateIndices[citation.properties.index] = true;
citation.properties.delete = true;
}
}
// delete uncited if neceessary
if(this.session.uncitedItems[item.key]) this.session.uncitedItems[item.key] = undefined;
}
/**
* Generates a preview of the bibliography entry for a given item
*/
Zotero.Integration.Session.BibliographyEditInterface.prototype.preview = function(item) {
var itemSet = this.session.style.createItemSet([item]);
return this.session.style.formatBibliography(itemSet, "Integration");
}