From 8268d1b01c7363107cba5cc7eb72d0c3c9beae46 Mon Sep 17 00:00:00 2001 From: Simon Kornblith Date: Tue, 14 Jun 2011 00:36:21 +0000 Subject: [PATCH] Zotero Everywhere megacommit - Implement connector for Firefox (should switch in/out of connector mode automatically when Standalone is launched or closed, although this has only been tested extensively on OS X) - Share core translation code between Zotero and connectors Still to be done: - Run translators in non-Fx connectors (this works in theory, but it's not currently enabled for any translators) - Show translation results in non-Fx connectors - Ability to translate to server when Zotero Standalone is not running --- chrome.manifest | 6 +- chrome/content/zotero/browser.js | 38 +- chrome/content/zotero/overlay.js | 41 +- chrome/content/zotero/recognizePDF.js | 8 +- chrome/content/zotero/xpcom/connector.js | 780 ------------------ .../zotero/xpcom/connector/cachedTypes.js | 113 +++ .../zotero/xpcom/connector/connector.js | 176 ++++ .../translate_item.js} | 18 +- .../zotero/xpcom/connector/translator.js | 341 ++++++++ chrome/content/zotero/xpcom/date.js | 2 +- chrome/content/zotero/xpcom/db.js | 31 +- chrome/content/zotero/xpcom/debug.js | 11 +- chrome/content/zotero/xpcom/integration.js | 333 +------- chrome/content/zotero/xpcom/ipc.js | 484 +++++++++++ .../content/zotero/xpcom/mimeTypeHandler.js | 3 + .../{integration_worker.js => pipe_worker.js} | 15 +- chrome/content/zotero/xpcom/proxy.js | 6 +- chrome/content/zotero/xpcom/server.js | 379 +++++++++ .../content/zotero/xpcom/server_connector.js | 607 ++++++++++++++ .../zotero/xpcom/translation/browser_other.js | 66 -- .../content/zotero/xpcom/translation/tlds.js | 271 ++++++ .../zotero/xpcom/translation/translate.js | 466 ++++++++--- ...rowser_firefox.js => translate_firefox.js} | 4 +- .../{item_local.js => translate_item.js} | 0 .../zotero/xpcom/translation/translator.js | 175 +++- chrome/content/zotero/xpcom/utilities.js | 49 +- chrome/content/zotero/xpcom/zotero.js | 310 +++++-- chrome/content/zotero/zoteroPane.js | 65 +- chrome/locale/en-US/zotero/zotero.properties | 4 +- ...vice.js => zotero-command-line-handler.js} | 81 +- components/zotero-protocol-handler.js | 16 +- components/zotero-service.js | 324 +++++--- defaults/preferences/zotero.js | 4 +- 33 files changed, 3582 insertions(+), 1645 deletions(-) delete mode 100755 chrome/content/zotero/xpcom/connector.js create mode 100644 chrome/content/zotero/xpcom/connector/cachedTypes.js create mode 100644 chrome/content/zotero/xpcom/connector/connector.js rename chrome/content/zotero/xpcom/{translation/item_connector.js => connector/translate_item.js} (67%) create mode 100644 chrome/content/zotero/xpcom/connector/translator.js create mode 100755 chrome/content/zotero/xpcom/ipc.js rename chrome/content/zotero/xpcom/{integration_worker.js => pipe_worker.js} (80%) create mode 100755 chrome/content/zotero/xpcom/server.js create mode 100755 chrome/content/zotero/xpcom/server_connector.js delete mode 100644 chrome/content/zotero/xpcom/translation/browser_other.js create mode 100644 chrome/content/zotero/xpcom/translation/tlds.js rename chrome/content/zotero/xpcom/translation/{browser_firefox.js => translate_firefox.js} (99%) rename chrome/content/zotero/xpcom/translation/{item_local.js => translate_item.js} (100%) rename components/{zotero-integration-service.js => zotero-command-line-handler.js} (58%) diff --git a/chrome.manifest b/chrome.manifest index 87a9631b8..99fba88f4 100644 --- a/chrome.manifest +++ b/chrome.manifest @@ -63,6 +63,6 @@ contract @mozilla.org/autocomplete/search;1?name=zotero {06a2ed11-d0a4-4ff0-a56 component {9BC3D762-9038-486A-9D70-C997AF848A7C} components/zotero-protocol-handler.js contract @mozilla.org/network/protocol;1?name=zotero {9BC3D762-9038-486A-9D70-C997AF848A7C} -component {531828f8-a16c-46be-b9aa-14845c3b010f} components/zotero-integration-service.js -contract @mozilla.org/commandlinehandler/general-startup;1?type=zotero-integration {531828f8-a16c-46be-b9aa-14845c3b010f} -category command-line-handler m-zotero-integration @mozilla.org/commandlinehandler/general-startup;1?type=zotero-integration +component {531828f8-a16c-46be-b9aa-14845c3b010f} components/zotero-command-line-handler.js +contract @mozilla.org/commandlinehandler/general-startup;1?type=zotero {531828f8-a16c-46be-b9aa-14845c3b010f} +category command-line-handler m-zotero @mozilla.org/commandlinehandler/general-startup;1?type=zotero diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js index 1bc6a17d1..e0f1b087d 100644 --- a/chrome/content/zotero/browser.js +++ b/chrome/content/zotero/browser.js @@ -112,6 +112,17 @@ var Zotero_Browser = new function() { function(e) { Zotero_Browser.chromeLoad(e) }, false); window.addEventListener("unload", function(e) { Zotero_Browser.chromeUnload(e) }, false); + + ZoteroPane_Local.addReloadListener(reload); + reload(); + } + + /** + * Called when Zotero is reloaded + */ + function reload() { + // Handles the display of a div showing progress in scraping + Zotero_Browser.progress = new Zotero.ProgressWindow(); } /** @@ -144,7 +155,7 @@ var Zotero_Browser = new function() { // get libraryID and collectionID var libraryID, collectionID; - if(ZoteroPane) { + if(ZoteroPane && !Zotero.isConnector) { libraryID = ZoteroPane.getSelectedLibraryID(); collectionID = ZoteroPane.getSelectedCollection(true); } else { @@ -374,7 +385,7 @@ var Zotero_Browser = new function() { // get data object var tab = _getTabObject(browser); - if(isHTML) { + if(isHTML && !Zotero.isConnector) { var annotationID = Zotero.Annotate.getAnnotationIDFromURL(browser.currentURI.spec); if(annotationID) { if(Zotero.Annotate.isAnnotated(annotationID)) { @@ -509,8 +520,8 @@ var Zotero_Browser = new function() { * Callback to be executed when an item has been finished */ function itemDone(obj, item, collection) { - var title = item.getField("title", false, true); - var icon = item.getImageSrc(); + var title = item.title; + var icon = Zotero.ItemTypes.getImageSrc(item.itemType); Zotero_Browser.progress.show(); Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scraping")); Zotero_Browser.progress.addLines([title], [icon]); @@ -742,7 +753,7 @@ Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID) { this.page.hasBeenTranslated = true; } this.page.translate.clearHandlers("itemDone"); - this.page.translate.setHandler("itemDone", function(obj, item) { Zotero_Browser.itemDone(obj, item, collection) }); + this.page.translate.setHandler("itemDone", function(obj, dbItem, item) { Zotero_Browser.itemDone(obj, item, collection) }); this.page.translate.translate(libraryID); } @@ -755,12 +766,10 @@ Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID) { Zotero_Browser.Tab.prototype.getCaptureIcon = function() { if(this.page.translators && this.page.translators.length) { var itemType = this.page.translators[0].itemType; - if(itemType == "multiple") { - // Use folder icon for multiple types, for now - return "chrome://zotero/skin/treesource-collection.png"; - } else { - return Zotero.ItemTypes.getImageSrc(itemType); - } + Zotero.debug("want capture icon for "+itemType); + return (itemType === "multiple" + ? "chrome://zotero/skin/treesource-collection.png" + : Zotero.ItemTypes.getImageSrc(itemType)); } return false; @@ -784,7 +793,7 @@ Zotero_Browser.Tab.prototype.getCaptureTooltip = function() { /* * called when a user is supposed to select items */ -Zotero_Browser.Tab.prototype._selectItems = function(obj, itemList) { +Zotero_Browser.Tab.prototype._selectItems = function(obj, itemList, callback) { // this is kinda ugly, mozillazine made me do it! honest! var io = { dataIn:itemList, dataOut:null } var newDialog = window.openDialog("chrome://zotero/content/ingester/selectitems.xul", @@ -794,7 +803,7 @@ Zotero_Browser.Tab.prototype._selectItems = function(obj, itemList) { Zotero_Browser.progress.close(); } - return io.dataOut; + callback(io.dataOut); } /* @@ -812,7 +821,4 @@ Zotero_Browser.Tab.prototype._translatorsAvailable = function(translate, transla Zotero_Browser.updateStatus(); } -// Handles the display of a div showing progress in scraping -Zotero_Browser.progress = new Zotero.ProgressWindow(); - Zotero_Browser.init(); \ No newline at end of file diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js index 553805da7..4ec88313d 100644 --- a/chrome/content/zotero/overlay.js +++ b/chrome/content/zotero/overlay.js @@ -29,11 +29,16 @@ var ZoteroOverlay = new function() { const DEFAULT_ZPANE_HEIGHT = 300; - var toolbarCollapseState, isFx36, showInPref; + var toolbarCollapseState, isFx36, showInPref; + var zoteroPane, zoteroSplitter; + var _stateBeforeReload = false; this.isTab = false; this.onLoad = function() { + zoteroPane = document.getElementById('zotero-pane-stack'); + zoteroSplitter = document.getElementById('zotero-splitter'); + ZoteroPane_Overlay = ZoteroPane; ZoteroPane.init(); @@ -141,6 +146,19 @@ var ZoteroOverlay = new function() if(Zotero.isFx4) { XULBrowserWindow.inContentWhitelist.push("chrome://zotero/content/tab.xul"); } + + // Close pane if connector is enabled + ZoteroPane_Local.addReloadListener(function() { + if(Zotero.isConnector) { + // save current state + _stateBeforeReload = !zoteroPane.hidden && !zoteroPane.collapsed; + // ensure pane is closed + if(!zoteroPane.collapsed) ZoteroOverlay.toggleDisplay(false); + } else { + // reopen pane if it was open before + ZoteroOverlay.toggleDisplay(_stateBeforeReload); + } + }); } this.onUnload = function() { @@ -158,10 +176,16 @@ var ZoteroOverlay = new function() */ this.toggleDisplay = function(makeVisible) { - if(this.isTab && (makeVisible || makeVisible === undefined)) { - // If in separate tab mode, just open the tab - this.loadZoteroTab(); - return; + if(makeVisible || makeVisible === undefined) { + if(Zotero.isConnector) { + // If in connector mode, bring Zotero Standalone to foreground + Zotero.activateStandalone(); + return; + } else if(this.isTab) { + // If in separate tab mode, just open the tab + this.loadZoteroTab(); + return; + } } if(!Zotero || !Zotero.initialized) { @@ -169,12 +193,7 @@ var ZoteroOverlay = new function() return; } - var zoteroPane = document.getElementById('zotero-pane-stack'); - var zoteroSplitter = document.getElementById('zotero-splitter'); - var isHidden = zoteroPane.getAttribute('hidden') == 'true'; - var isCollapsed = zoteroPane.getAttribute('collapsed') == 'true'; - - if(makeVisible === undefined) makeVisible = isHidden || isCollapsed; + if(makeVisible === undefined) makeVisible = zoteroPane.hidden || zoteroPane.collapsed; zoteroSplitter.setAttribute('hidden', !makeVisible); zoteroPane.setAttribute('hidden', false); diff --git a/chrome/content/zotero/recognizePDF.js b/chrome/content/zotero/recognizePDF.js index 7079b938b..256a95b9b 100644 --- a/chrome/content/zotero/recognizePDF.js +++ b/chrome/content/zotero/recognizePDF.js @@ -402,7 +402,7 @@ Zotero_RecognizePDF.Recognizer.prototype._queryGoogle = function() { Zotero.Browser.deleteHiddenBrowser(me._hiddenBrowser); me._callback(item); }); - translate.setHandler("select", function(translate, items) { return me._selectItems(translate, items) }); + translate.setHandler("select", function(translate, items) { me._selectItems(translate, items, callback) }); translate.setHandler("done", function(translate, success) { if(!success) me._queryGoogle(); }); this._hiddenBrowser.addEventListener("pageshow", function() { me._scrape(translate) }, true); @@ -449,10 +449,12 @@ Zotero_RecognizePDF.Recognizer.prototype._scrape = function(/**Zotero.Translate* * @private * @type Object */ -Zotero_RecognizePDF.Recognizer.prototype._selectItems = function(/**Zotero.Translate*/ translate, /**Object*/ items) { +Zotero_RecognizePDF.Recognizer.prototype._selectItems = function(/**Zotero.Translate*/ translate, + /**Object*/ items, /**Function**/ callback) { for(var i in items) { var obj = {}; obj[i] = items; - return obj; + callback(obj); + return; } } \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector.js b/chrome/content/zotero/xpcom/connector.js deleted file mode 100755 index 5ada86a06..000000000 --- a/chrome/content/zotero/xpcom/connector.js +++ /dev/null @@ -1,780 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://zotero.org - - This file is part of Zotero. - - Zotero is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Zotero is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Zotero. If not, see . - - ***** END LICENSE BLOCK ***** -*/ - -Zotero.Connector = new function() { - var _onlineObserverRegistered; - var responseCodes = { - 200:"OK", - 201:"Created", - 300:"Multiple Choices", - 400:"Bad Request", - 404:"Not Found", - 500:"Internal Server Error", - 501:"Method Not Implemented" - }; - - /** - * initializes a very rudimentary web server - */ - this.init = function() { - if (Zotero.HTTP.browserIsOffline()) { - Zotero.debug('Browser is offline -- not initializing connector 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('connector.port'), true, -1); - serv.asyncListen(Zotero.Connector.SocketListener); - - Zotero.debug("Connector HTTP server listening on 127.0.0.1:"+serv.port); - } catch(e) { - Zotero.debug("Not initializing connector HTTP server"); - } - - _registerOnlineObserver() - } - - /** - * generates the response to an HTTP request - */ - this.generateResponse = function (status, contentType, body) { - var response = "HTTP/1.0 "+status+" "+responseCodes[status]+"\r\n"; - response += "Access-Control-Allow-Origin: org.zotero.zoteroconnectorforsafari-69x6c999f9\r\n"; - response += "Access-Control-Allow-Methods: POST, GET, OPTIONS, HEAD\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; - } - - /** - * Decodes application/x-www-form-urlencoded data - * - * @param {String} postData application/x-www-form-urlencoded data, as sent in a g request - * @return {Object} data in object form - */ - this.decodeURLEncodedData = function(postData) { - var splitData = postData.split("&"); - var variables = {}; - for each(var variable in splitData) { - var splitIndex = variable.indexOf("="); - variables[decodeURIComponent(variable.substr(0, splitIndex))] = decodeURIComponent(variable.substr(splitIndex+1)); - } - return variables; - } - - 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.Connector.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.Connector.SocketListener = new function() { - this.onSocketAccepted = onSocketAccepted; - this.onStopListening = onStopListening; - - /* - * called when a socket is opened - */ - function onSocketAccepted(socket, transport) { - // get an input stream - var iStream = transport.openInputStream(0, 0, 0); - var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0); - - var dataListener = new Zotero.Connector.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("Connector HTTP server going offline"); - } -} - -/* - * handles the actual acquisition of data - */ -Zotero.Connector.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.Connector.DataListener.prototype.onStartRequest = function(request, context) {} - -/* - * called when a request stops - */ -Zotero.Connector.DataListener.prototype.onStopRequest = function(request, context, status) { - this.iStream.close(); - this.oStream.close(); -} - -/* - * called when new data is available - */ -Zotero.Connector.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.Connector.DataListener.prototype._headerFinished = function() { - this.headerFinished = true; - - Zotero.debug(this.header); - - const methodRe = /^([A-Z]+) ([^ \r\n?]+)(\?[^ \r\n]+)?/; - - // get first line of request (all we care about for now) - var method = methodRe.exec(this.header); - - if(!method) { - this._requestFinished(Zotero.Connector.generateResponse(400)); - return; - } - if(!Zotero.Connector.Endpoints[method[2]]) { - this._requestFinished(Zotero.Connector.generateResponse(404)); - return; - } - this.endpoint = Zotero.Connector.Endpoints[method[2]]; - - if(method[1] == "HEAD" || method[1] == "OPTIONS") { - this._requestFinished(Zotero.Connector.generateResponse(200)); - } else if(method[1] == "GET") { - this._requestFinished(this._processEndpoint("GET", method[3])); - } else if(method[1] == "POST") { - const contentLengthRe = /[\r\n]Content-Length: *([0-9]+)/i; - - // parse content length - var m = contentLengthRe.exec(this.header); - if(!m) { - this._requestFinished(Zotero.Connector.generateResponse(400)); - return; - } - - this.bodyLength = parseInt(m[1]); - this._bodyData(); - } else { - this._requestFinished(Zotero.Connector.generateResponse(501)); - return; - } -} - -/* - * checks to see if Content-Length bytes of body have been read and, if so, processes the body - */ -Zotero.Connector.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 - this._processEndpoint("POST", this.body); - } -} - -/** - * Generates a response based on calling the function associated with the endpoint - */ -Zotero.Connector.DataListener.prototype._processEndpoint = function(method, postData) { - try { - var endpoint = new this.endpoint; - var me = this; - var sendResponseCallback = function(code, contentType, arg) { - me._requestFinished(Zotero.Connector.generateResponse(code, contentType, arg)); - } - endpoint.init(method, postData ? postData : undefined, sendResponseCallback); - } catch(e) { - Zotero.debug(e); - this._requestFinished(Zotero.Connector.generateResponse(500)); - throw e; - } -} - -/* - * returns HTTP data from a request - */ -Zotero.Connector.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 - Zotero.debug(response); - intlStream.writeString(response); - } finally { - intlStream.close(); - } -} - -/** - * Manage cookies in a sandboxed fashion - * - * @param {browser} browser Hidden browser object - * @param {String} uri URI of page to manage cookies for (cookies for domains that are not - * subdomains of this URI are ignored) - * @param {String} cookieData Cookies with which to initiate the sandbox - */ -Zotero.Connector.CookieManager = function(browser, uri, cookieData) { - this._webNav = browser.webNavigation; - this._browser = browser; - this._watchedBrowsers = [browser]; - this._observerService = Components.classes["@mozilla.org/observer-service;1"]. - getService(Components.interfaces.nsIObserverService); - - this._uri = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService) - .newURI(uri, null, null); - - var splitCookies = cookieData.split(/; ?/); - this._cookies = {}; - for each(var cookie in splitCookies) { - var key = cookie.substr(0, cookie.indexOf("=")); - var value = cookie.substr(cookie.indexOf("=")+1); - this._cookies[key] = value; - } - - [this._observerService.addObserver(this, topic, false) for each(topic in this._observerTopics)]; -} - -Zotero.Connector.CookieManager.prototype = { - "_observerTopics":["http-on-examine-response", "http-on-modify-request", "quit-application"], - "_watchedXHRs":[], - - /** - * nsIObserver implementation for adding, clearing, and slurping cookies - */ - "observe": function(channel, topic) { - if(topic == "quit-application") { - Zotero.debug("WARNING: A Zotero.Connector.CookieManager for "+this._uri.spec+" was still open on shutdown"); - } else { - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - var isTracked = null; - try { - var topDoc = channel.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document; - for each(var browser in this._watchedBrowsers) { - isTracked = topDoc == browser.contentDocument; - if(isTracked) break; - } - } catch(e) {} - if(isTracked === null) { - try { - isTracked = channel.loadGroup.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document == this._browser.contentDocument; - } catch(e) {} - } - if(isTracked === null) { - try { - isTracked = this._watchedXHRs.indexOf(channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest)) !== -1; - } catch(e) {} - } - - // isTracked is now either true, false, or null - // true => we should manage cookies for this request - // false => we should not manage cookies for this request - // null => this request is of a type we couldn't match to this request. one such type - // is a link prefetch (nsPrefetchNode) but there might be others as well. for - // now, we are paranoid and reject these. - - if(isTracked === false) { - Zotero.debug("Zotero.Connector.CookieManager: not touching channel for "+channel.URI.spec); - return; - } else if(isTracked) { - Zotero.debug("Zotero.Connector.CookieManager: managing cookies for "+channel.URI.spec); - } else { - Zotero.debug("Zotero.Connector.CookieManager: being paranoid about channel for "+channel.URI.spec); - } - - if(topic == "http-on-modify-request") { - // clear cookies to be sent to other domains - if(isTracked === null || channel.URI.host != this._uri.host) { - channel.setRequestHeader("Cookie", "", false); - channel.setRequestHeader("Cookie2", "", false); - Zotero.debug("Zotero.Connector.CookieManager: cleared cookies to be sent to "+channel.URI.spec); - return; - } - - // add cookies to be sent to this domain - var cookies = [key+"="+this._cookies[key] - for(key in this._cookies)].join("; "); - channel.setRequestHeader("Cookie", cookies, false); - Zotero.debug("Zotero.Connector.CookieManager: added cookies for request to "+channel.URI.spec); - } else if(topic == "http-on-examine-response") { - // clear cookies being received - try { - var cookieHeader = channel.getResponseHeader("Set-Cookie"); - } catch(e) { - return; - } - channel.setResponseHeader("Set-Cookie", "", false); - channel.setResponseHeader("Set-Cookie2", "", false); - - // don't process further if these cookies are for another set of domains - if(isTracked === null || channel.URI.host != this._uri.host) { - Zotero.debug("Zotero.Connector.CookieManager: rejected cookies from "+channel.URI.spec); - return; - } - - // put new cookies into our sandbox - if(cookieHeader) { - var cookies = cookieHeader.split(/; ?/); - var newCookies = {}; - for each(var cookie in cookies) { - var key = cookie.substr(0, cookie.indexOf("=")); - var value = cookie.substr(cookie.indexOf("=")+1); - var lcCookie = key.toLowerCase(); - - if(["comment", "domain", "max-age", "path", "version", "expires"].indexOf(lcCookie) != -1) { - // ignore cookie parameters; we are only holding cookies for a few minutes - // with a single domain, and the path attribute doesn't allow any additional - // security anyway - continue; - } else if(lcCookie == "secure") { - // don't accept secure cookies - newCookies = {}; - break; - } else { - newCookies[key] = value; - } - } - [this._cookies[key] = newCookies[key] for(key in newCookies)]; - } - - Zotero.debug("Zotero.Connector.CookieManager: slurped cookies from "+channel.URI.spec); - } - } - }, - - /** - * Attach CookieManager to a specific XMLHttpRequest - * @param {XMLHttpRequest} xhr - */ - "attachToBrowser": function(browser) { - this._watchedBrowsers.push(browser); - }, - - /** - * Attach CookieManager to a specific XMLHttpRequest - * @param {XMLHttpRequest} xhr - */ - "attachToXHR": function(xhr) { - this._watchedXHRs.push(xhr); - }, - - /** - * Destroys this CookieManager (intended to be executed when the browser is destroyed) - */ - "destroy": function() { - [this._observerService.removeObserver(this, topic) for each(topic in this._observerTopics)]; - } -} - -Zotero.Connector.Data = {}; - -Zotero.Connector.Translate = function() {}; -Zotero.Connector.Translate._waitingForSelection = {}; - - -/** - * Lists all available translators, including code for translators that should be run on every page - */ -Zotero.Connector.Translate.List = function() {}; - -Zotero.Connector.Translate.List.prototype = { - /** - * Gets available translator list - * @param {String} method "GET" or "POST" - * @param {String} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response - */ - "init":function(method, data, sendResponseCallback) { - if(method != "POST") { - sendResponseCallback(400); - return; - } - - var translators = Zotero.Translators.getAllForType("web"); - var jsons = []; - for each(var translator in translators) { - let json = {}; - for each(var key in ["translatorID", "label", "creator", "target", "priority", "detectXPath"]) { - json[key] = translator[key]; - } - json["localExecution"] = translator.browserSupport.indexOf(data["browser"]) !== -1; - - // Do not pass targetless translators that do not support this browser (since that - // would mean passing each page back to Zotero) - if(json["target"] || json["detectXPath"] || json["localExecution"]) { - jsons.push(json); - } - } - - sendResponseCallback(200, "application/json", JSON.stringify(jsons)); - } -} - -/** - * Detects whether there is an available translator to handle a given page - */ -Zotero.Connector.Translate.Detect = function() {}; - -Zotero.Connector.Translate.Detect.prototype = { - /** - * Loads HTML into a hidden browser and initiates translator detection - * @param {String} method "GET" or "POST" - * @param {String} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response - */ - "init":function(method, data, sendResponseCallback) { - if(method != "POST") { - sendResponseCallback(400); - return; - } - - this.sendResponse = sendResponseCallback; - this._parsedPostData = JSON.parse(data); - - this._translate = new Zotero.Translate("web"); - this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) }); - - Zotero.Connector.Data[this._parsedPostData["uri"]] = ""+this._parsedPostData["html"]+""; - this._browser = Zotero.Browser.createHiddenBrowser(); - - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null); - - var pageShowCalled = false; - var me = this; - this._translate.setCookieManager(new Zotero.Connector.CookieManager(this._browser, - this._parsedPostData["uri"], this._parsedPostData["cookie"])); - this._browser.addEventListener("DOMContentLoaded", function() { - try { - if(me._browser.contentDocument.location.href == "about:blank") return; - if(pageShowCalled) return; - pageShowCalled = true; - delete Zotero.Connector.Data[me._parsedPostData["uri"]]; - - // get translators - me._translate.setDocument(me._browser.contentDocument); - me._translate.getTranslators(); - } catch(e) { - Zotero.debug(e); - throw e; - } - }, false); - - me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"])); - }, - - /** - * Callback to be executed when list of translators becomes available. Sends response with - * item types, translator IDs, labels, and icons for available translators. - * @param {Zotero.Translate} translate - * @param {Zotero.Translator[]} translators - */ - "_translatorsAvailable":function(obj, translators) { - var jsons = []; - for each(var translator in translators) { - if(translator.itemType == "multiple") { - var icon = "treesource-collection.png" - } else { - var icon = Zotero.ItemTypes.getImageSrc(translator.itemType); - icon = icon.substr(icon.lastIndexOf("/")+1); - } - var json = {"itemType":translator.itemType, "translatorID":translator.translatorID, - "label":translator.label, "icon":icon} - jsons.push(json); - } - this.sendResponse(200, "application/json", JSON.stringify(jsons)); - - this._translate.cookieManager.destroy(); - Zotero.Browser.deleteHiddenBrowser(this._browser); - } -} - -/** - * Performs translation of a given page - */ -Zotero.Connector.Translate.Save = function() {}; -Zotero.Connector.Translate.Save.prototype = { - /** - * Init method inherited from Zotero.Connector.Translate.Detect - * @borrows Zotero.Connector.Translate.Detect as this.init - */ - "init":Zotero.Connector.Translate.Detect.prototype.init, - - /** - * Callback to be executed when items must be selected - * @param {Zotero.Translate} translate - * @param {Object} itemList ID=>text pairs representing available items - */ - "_selectItems":function(translate, itemList) { - var instanceID = Zotero.randomString(); - Zotero.Connector.Translate._waitingForSelection[instanceID] = this; - - // Fix for translators that don't create item lists as objects - if(itemList.push && typeof itemList.push === "function") { - var newItemList = {}; - for(var item in itemList) { - Zotero.debug(item); - newItemList[item] = itemList[item]; - } - itemList = newItemList; - } - - // Send "Multiple Choices" HTTP response - this.sendResponse(300, "application/json", JSON.stringify({"items":itemList, "instanceID":instanceID, "uri":this._parsedPostData.uri})); - - // We need this to make sure that we won't stop Firefox from quitting, even if the user - // didn't close the selectItems window - var observerService = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - var me = this; - var quitObserver = {observe:function() { me.selectedItems = false; }}; - observerService.addObserver(quitObserver, "quit-application", false); - - this.selectedItems = null; - var endTime = Date.now() + 60*60*1000; // after an hour, timeout, so that we don't - // permanently slow Firefox with this loop - while(this.selectedItems === null && Date.now() < endTime) { - Zotero.mainThread.processNextEvent(true); - } - - observerService.removeObserver(quitObserver, "quit-application"); - if(!this.selectedItems) this._progressWindow.close(); - return this.selectedItems; - }, - - /** - * Callback to be executed when list of translators becomes available. Opens progress window, - * selects specified translator, and initiates translation. - * @param {Zotero.Translate} translate - * @param {Zotero.Translator[]} translators - */ - "_translatorsAvailable":function(translate, translators) { - // make sure translatorsAvailable succeded - if(!translators.length) { - Zotero.Browser.deleteHiddenBrowser(this._browser); - this.sendResponse(500); - return; - } - - // set up progress window - var win = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator) - .getMostRecentWindow("navigator:browser"); - - this._progressWindow = win.Zotero_Browser.progress; - if(Zotero.locked) { - this._progressWindow.changeHeadline(Zotero.getString("ingester.scrapeError")); - var desc = Zotero.localeJoin([ - Zotero.getString('general.operationInProgress'), Zotero.getString('general.operationInProgress.waitUntilFinishedAndTryAgain') - ]); - this._progressWindow.addDescription(desc); - this._progressWindow.show(); - this._progressWindow.startCloseTimer(8000); - return; - } - - this._progressWindow.show(); - - // set save callbacks - this._libraryID = null; - var collection = null; - try { - this._libraryID = win.ZoteroPane.getSelectedLibraryID(); - collection = win.ZoteroPane.getSelectedCollection(); - } catch(e) {} - var me = this; - translate.setHandler("select", function(obj, item) { return me._selectItems(obj, item) }); - translate.setHandler("itemDone", function(obj, item) { win.Zotero_Browser.itemDone(obj, item, collection) }); - translate.setHandler("done", function(obj, item) { - win.Zotero_Browser.finishScraping(obj, item, collection); - me._translate.cookieManager.destroy(); - Zotero.Browser.deleteHiddenBrowser(me._browser); - me.sendResponse(201); - }); - - // set translator and translate - translate.setTranslator(this._parsedPostData.translatorID); - translate.translate(this._libraryID); - } -} - -/** - * Handle item selection - */ -Zotero.Connector.Translate.Select = function() {}; -Zotero.Connector.Translate.Select.prototype = { - /** - * Finishes up translation when item selection is complete - * @param {String} method "GET" or "POST" - * @param {String} data POST data or GET query string - * @param {Function} sendResponseCallback function to send HTTP response - */ - "init":function(method, postData, sendResponseCallback) { - if(method != "POST") { - sendResponseCallback(400); - return; - } - - var postData = JSON.parse(postData); - var saveInstance = Zotero.Connector.Translate._waitingForSelection[postData.instanceID]; - saveInstance.sendResponse = sendResponseCallback; - - saveInstance.selectedItems = false; - for(var i in postData.items) { - saveInstance.selectedItems = postData.items; - break; - } - } -} - -/** - * Endpoints for the Connector HTTP server - * - * Each endpoint should take the form of an object. The init() method of this object will be passed: - * method - the method of the request ("GET" or "POST") - * data - the query string (for a "GET" request) or POST data (for a "POST" request) - * sendResponseCallback - a function to send a response to the HTTP request. This can be passed - * a response code alone (e.g., sendResponseCallback(404)) or a response - * code, MIME type, and response body - * (e.g., sendResponseCallback(200, "text/plain", "Hello World!")) - */ -Zotero.Connector.Endpoints = { - "/translate/list":Zotero.Connector.Translate.List, - "/translate/detect":Zotero.Connector.Translate.Detect, - "/translate/save":Zotero.Connector.Translate.Save, - "/translate/select":Zotero.Connector.Translate.Select -} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/cachedTypes.js b/chrome/content/zotero/xpcom/connector/cachedTypes.js new file mode 100644 index 000000000..740c59f5b --- /dev/null +++ b/chrome/content/zotero/xpcom/connector/cachedTypes.js @@ -0,0 +1,113 @@ +/* + ***** 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +/** + * Emulates very small parts of cachedTypes.js and itemFields.js APIs for use with connector + */ + +/** + * @namespace + */ +Zotero.Connector.Types = new function() { + /** + * Initializes types + * @param {Object} typeSchema typeSchema generated by Zotero.Connector.GetData#_generateTypeSchema + */ + this.init = function(typeSchema) { + const schemaTypes = ["itemTypes", "creatorTypes", "fields"]; + + // attach IDs and make referenceable by either ID or name + for(var i=0; i. + + ***** END LICENSE BLOCK ***** +*/ +Zotero.Connector = new function() { + const CONNECTOR_URI = "http://127.0.0.1:23119/"; + + this.isOnline = true; + this.haveRefreshedData = false; + this.data = null; + + /** + * Called to initialize Zotero + */ + this.init = function() { + Zotero.Connector.getData(); + } + + function _getDataFile() { + var dataFile = Zotero.getZoteroDirectory(); + dataFile.append("connector.json"); + return dataFile; + } + + /** + * Serializes the Zotero.Connector.data object to localStorage/preferences + * @param {String} [json] The + */ + this.serializeData = function(json) { + if(!json) json = JSON.stringify(Zotero.Connector.data); + + if(Zotero.isFx) { + Zotero.File.putContents(_getDataFile(), json); + } else { + localStorage.data = json; + } + } + + /** + * Unserializes the Zotero.Connector.data object from localStorage/preferences + */ + this.unserializeData = function() { + var data = null; + + if(Zotero.isFx) { + var dataFile = _getDataFile(); + if(dataFile.exists()) data = Zotero.File.getContents(dataFile); + } else { + if(localStorage.data) data = localStorage.data; + } + + if(data) Zotero.Connector.data = JSON.parse(data); + } + + // saner descriptions of some HTTP error codes + this.EXCEPTION_NOT_AVAILABLE = 0; + this.EXCEPTION_BAD_REQUEST = 400; + this.EXCEPTION_NO_ENDPOINT = 404; + this.EXCEPTION_CONNECTOR_INTERNAL = 500; + this.EXCEPTION_METHOD_NOT_IMPLEMENTED = 501; + this.EXCEPTION_CODES = [0, 400, 404, 500, 501]; + + /** + * Updates Zotero's status depending on the success or failure of a request + * + * @param {Boolean} isOnline Whether or not Zotero was online + * @param {Function} successCallback Function to be called after loading new data if + * Zotero is online + * @param {Function} failureCallback Function to be called if Zotero is offline + * + * Calls Zotero.Connector.Browser.onStateChange(isOnline, method, context) if status has changed + */ + function _checkState(isOnline, callback) { + if(isOnline) { + if(Zotero.Connector.haveRefreshedData) { + if(callback) callback(true); + } else { + Zotero.Connector.getData(callback); + } + } else { + if(callback) callback(false, this.EXCEPTION_NOT_AVAILABLE); + } + + if(Zotero.Connector.isOnline !== isOnline) { + Zotero.Connector.isOnline = isOnline; + if(Zotero.Connector_Browser && Zotero.Connector_Browser.onStateChange) { + Zotero.Connector_Browser.onStateChange(isOnline); + } + } + + return isOnline; + } + + /** + * Loads list of translators and other relevant data from local Zotero instance + * + * @param {Function} successCallback Function to be called after loading new data if + * Zotero is online + * @param {Function} failureCallback Function to be called if Zotero is offline + */ + this.getData = function(callback) { + Zotero.HTTP.doPost(CONNECTOR_URI+"connector/getData", + JSON.stringify({"browser":Zotero.Connector_Browser}), + function(req) { + var isOnline = req.status !== 0; + + if(isOnline) { + // if request succeded, update data + Zotero.Connector.haveRefreshedData = true; + Zotero.Connector.serializeData(req.responseText); + Zotero.Connector.data = JSON.parse(req.responseText); + } else { + // if request failed, unserialize saved data + Zotero.Connector.unserializeData(); + } + Zotero.Connector.Types.init(Zotero.Connector.data.schema); + + // update online state. this shouldn't loop, since haveRefreshedData should + // be true if isOnline is true. + _checkState(isOnline, callback); + }, {"Content-Type":"application/json"}); + } + + /** + * Sends the XHR to execute an RPC call. + * + * @param {String} method RPC method. See documentation above. + * @param {Object} data RPC data. See documentation above. + * @param {Function} successCallback Function to be called if request succeeded. + * @param {Function} failureCallback Function to be called if request failed. + */ + this.callMethod = function(method, data, callback) { + Zotero.HTTP.doPost(CONNECTOR_URI+"connector/"+method, JSON.stringify(data), + function(req) { + _checkState(req.status != 0, function() { + if(!callback) callback(false); + + if(Zotero.Connector.EXCEPTION_CODES.indexOf(req.status) !== -1) { + if(callback) callback(false, req.status); + } else { + if(callback) { + var val = undefined; + if(req.responseText) { + if(req.getResponseHeader("Content-Type") === "application/json") { + val = JSON.parse(req.responseText); + } else { + val = req.responseText; + } + } + callback(val, req.status); + } + } + }); + }, {"Content-Type":"application/json"}); + } +} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/item_connector.js b/chrome/content/zotero/xpcom/connector/translate_item.js similarity index 67% rename from chrome/content/zotero/xpcom/translation/item_connector.js rename to chrome/content/zotero/xpcom/connector/translate_item.js index 331e2094e..f233e4b99 100644 --- a/chrome/content/zotero/xpcom/translation/item_connector.js +++ b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -23,8 +23,18 @@ ***** END LICENSE BLOCK ***** */ -Zotero.Translate.Item = { - "saveItem":function (translate, item) { - +Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) { + this.newItems = []; +} + +Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0; +Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1; +Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2; + +Zotero.Translate.ItemSaver.prototype = { + "saveItem":function(item) { + this.newItems.push(item); + Zotero.debug("Saving item"); + Zotero.Connector.callMethod("saveItems", {"items":[item]}); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/translator.js b/chrome/content/zotero/xpcom/connector/translator.js new file mode 100644 index 000000000..92d483775 --- /dev/null +++ b/chrome/content/zotero/xpcom/connector/translator.js @@ -0,0 +1,341 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +// Enumeration of types of translators +const TRANSLATOR_TYPES = {"import":1, "export":2, "web":4, "search":8}; + +/** + * Singleton to handle loading and caching of translators + * @namespace + */ +Zotero.Translators = new function() { + var _cache, _translators; + var _initialized = false; + + /** + * Initializes translator cache, loading all relevant translators into memory + */ + this.init = function() { + _cache = {"import":[], "export":[], "web":[], "search":[]}; + _translators = {}; + _initialized = true; + + // Build caches + var translators = Zotero.Connector.data.translators; + for(var i=0; i b.priority) { + return 1; + } + else if (a.priority < b.priority) { + return -1; + } + } + for(var type in _cache) { + _cache[type].sort(cmp); + } + } + + /** + * Gets the translator that corresponds to a given ID + * @param {String} id The ID of the translator + * @param {Function} [callback] An optional callback to be executed when translators have been + * retrieved. If no callback is specified, translators are + * returned. + */ + this.get = function(id, callback) { + if(!_initialized) Zotero.Translators.init(); + var translator = _translators[id]; + if(!translator) { + callback(false); + return false; + } + + // only need to get code if it is of some use + if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { + translator.getCode(function() { callback(translator) }); + } else { + callback(translator); + } + } + + /** + * Gets all translators for a specific type of translation + * @param {String} type The type of translators to get (import, export, web, or search) + * @param {Function} [callback] An optional callback to be executed when translators have been + * retrieved. If no callback is specified, translators are + * returned. + */ + this.getAllForType = function(type, callback) { + if(!_initialized) Zotero.Translators.init() + var translators = _cache[type].slice(0); + new Zotero.Translators.CodeGetter(translators, callback, translators); + return true; + } + + /** + * Gets web translators for a specific location + * @param {String} uri The URI for which to look for translators + * @param {Function} [callback] An optional callback to be executed when translators have been + * retrieved. If no callback is specified, translators are + * returned. The callback is passed a set of functions for + * converting URLs from proper to proxied forms as the second + * argument. + */ + this.getWebTranslatorsForLocation = function(uri, callback) { + if(!_initialized) Zotero.Translators.init(); + var allTranslators = _cache["web"]; + var potentialTranslators = []; + var searchURIs = [uri]; + + Zotero.debug("Translators: Looking for translators for "+uri); + + // if there is a subdomain that is also a TLD, also test against URI with the domain + // dropped after the TLD + // (i.e., www.nature.com.mutex.gmu.edu => www.nature.com) + var m = /^(https?:\/\/)([^\/]+)/i.exec(uri); + var properHosts = []; + var proxyHosts = []; + if(m) { + var hostnames = m[2].split("."); + for(var i=1; i4 supports deferred open; no need to use sh - var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. - createInstance(Components.interfaces.nsIFileInputStream); - fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream); - // 16 = open as deferred so that we don't block on open - fifoStream.init(_fifoFile, -1, 0, 16); - - var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]. - createInstance(Components.interfaces.nsIInputStreamPump); - pump.init(fifoStream, -1, -1, 4096, 1, true); - pump.asyncRead(_integrationPipeListenerFx42, null); - } - - /** - * Initializes the Zotero Integration Pipe - */ - function _initializeIntegrationPipe() { - var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"] - .getService(Components.interfaces.nsIVersionComparator); - var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]. - getService(Components.interfaces.nsIXULAppInfo); - if(Zotero.isFx4) { - if(verComp.compare("2.0b9pre", appInfo.platformVersion) > 0) { - Components.utils.reportError("Zotero word processor integration requires "+ - "Firefox 4.0b9 or later. Please update to the latest Firefox 4.0 beta."); - return; - } else if(verComp.compare("2.2a1pre", appInfo.platformVersion) <= 0) { - _pipeMode = "deferredOpen"; - } else { - _pipeMode = "fx4thread"; - } - } else { - if(Zotero.isMac) { - _pipeMode = "poll"; - } else { - _pipeMode = "fx36thread"; - } - } - - Zotero.debug("Using integration pipe mode "+_pipeMode); - - if(_pipeMode === "poll") { - // create empty file - var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. - createInstance(Components.interfaces.nsIFileOutputStream); - foStream.init(_fifoFile, 0x02 | 0x08 | 0x20, 0666, 0); - foStream.close(); - - // no deferred open capability, so we need to poll - // has to be global so that we don't get garbage collected - _timer = Components.classes["@mozilla.org/timer;1"]. - createInstance(Components.interfaces.nsITimer); - _timer.initWithCallback(_integrationPipeObserverFx36, 1000, - Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); - } else { - // make a new pipe - var mkfifo = Components.classes["@mozilla.org/file/local;1"]. - createInstance(Components.interfaces.nsILocalFile); - mkfifo.initWithPath("/usr/bin/mkfifo"); - if(!mkfifo.exists()) mkfifo.initWithPath("/bin/mkfifo"); - if(!mkfifo.exists()) mkfifo.initWithPath("/usr/local/bin/mkfifo"); - - if(mkfifo.exists()) { - // create named pipe - var proc = Components.classes["@mozilla.org/process/util;1"]. - createInstance(Components.interfaces.nsIProcess); - proc.init(mkfifo); - proc.run(true, [_fifoFile.path], 1); - - if(_fifoFile.exists()) { - if(_pipeMode === "deferredOpen") { - _initializePipeStreamPump(); - } else if(_pipeMode === "fx36thread") { - var main = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread; - var background = Components.classes["@mozilla.org/thread-manager;1"].getService().newThread(0); - - function mainThread(agent, cmd, doc) { - this.agent = agent; - this.cmd = cmd; - this.document = doc; - } - mainThread.prototype.run = function() { - Zotero.Integration.execCommand(this.agent, this.cmd, this.document); - } - - function fifoThread() {} - fifoThread.prototype.run = function() { - var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. - createInstance(Components.interfaces.nsIFileInputStream); - var line = {}; - while(true) { - fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream); - fifoStream.init(_fifoFile, -1, 0, 0); - fifoStream.QueryInterface(Components.interfaces.nsILineInputStream); - fifoStream.readLine(line); - fifoStream.close(); - - var parts = line.value.split(" "); - var agent = parts[0]; - var cmd = parts[1]; - var document = parts.length >= 3 ? line.value.substr(agent.length+cmd.length+2) : null; - if(agent == "Zotero" && cmd == "shutdown") return; - main.dispatch(new mainThread(agent, cmd, document), background.DISPATCH_NORMAL); - } - } - - fifoThread.prototype.QueryInterface = mainThread.prototype.QueryInterface = function(iid) { - if (iid.equals(Components.interfaces.nsIRunnable) || - iid.equals(Components.interfaces.nsISupports)) return this; - throw Components.results.NS_ERROR_NO_INTERFACE; - } - - background.dispatch(new fifoThread(), background.DISPATCH_NORMAL); - } else if(_pipeMode === "fx4thread") { - Components.utils.import("resource://gre/modules/ctypes.jsm"); - - // get possible names for libc - if(Zotero.isMac) { - var possibleLibcs = ["/usr/lib/libc.dylib"]; - } else { - var possibleLibcs = [ - "libc.so.6", - "libc.so.6.1", - "libc.so" - ]; - } - - // try all possibilities - while(possibleLibcs.length) { - var libc = possibleLibcs.shift(); - try { - var lib = ctypes.open(libc); - break; - } catch(e) {} - } - - // throw appropriate error on failure - if(!lib) { - throw "libc could not be loaded. Please post on the Zotero Forums so we can add "+ - "support for your operating system."; - } - - // int mkfifo(const char *path, mode_t mode); - var mkfifo = lib.declare("mkfifo", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.unsigned_int); - - // make pipe - var ret = mkfifo(_fifoFile.path, 0600); - if(!_fifoFile.exists()) return false; - lib.close(); - - // set up worker - var worker = Components.classes["@mozilla.org/threads/workerfactory;1"] - .createInstance(Components.interfaces.nsIWorkerFactory) - .newChromeWorker("chrome://zotero/content/xpcom/integration_worker.js"); - worker.onmessage = function(event) { - if(event.data[0] == "Exception") { - throw event.data[1]; - } else if(event.data[0] == "Debug") { - Zotero.debug(event.data[1]); - } else { - Zotero.Integration.execCommand(event.data[0], event.data[1], event.data[2]); - } - } - worker.postMessage({"path":_fifoFile.path, "libc":libc}); - } - } else { - Components.utils.reportError("Zotero: mkfifo failed -- not initializing integration pipe"); - return false; - } - } else { - Components.utils.reportError("Zotero: mkfifo or sh not found -- not initializing integration pipe"); - return false; - } - } - - return true; - } - /** * Calls the Integration applicatoon */ @@ -546,22 +305,6 @@ Zotero.Integration = new function() { } } - /** - * Destroys the integration pipe. - */ - this.destroy = function() { - if(_pipeMode !== "poll") { - // send shutdown message to fifo thread - var oStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. - getService(Components.interfaces.nsIFileOutputStream); - oStream.init(_fifoFile, 0x02 | 0x10, 0, 0); - var cmd = "Zotero shutdown\n"; - oStream.write(cmd, cmd.length); - oStream.close(); - } - _fifoFile.remove(false); - } - /** * Activates Firefox */ diff --git a/chrome/content/zotero/xpcom/ipc.js b/chrome/content/zotero/xpcom/ipc.js new file mode 100755 index 000000000..f08f293fd --- /dev/null +++ b/chrome/content/zotero/xpcom/ipc.js @@ -0,0 +1,484 @@ +/* + ***** 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.IPC = new function() { + var _libc, _libcPath, _instancePipe, _user32; + + /** + * Initialize pipe for communication with connector + */ + this.init = function() { + if(!Zotero.isWin && (Zotero.isFx4 || Zotero.isMac)) { // no pipe support on Fx 3.6 + _instancePipe = _getPipeDirectory(); + if(!_instancePipe.exists()) { + _instancePipe.create(Ci.nsIFile.DIRECTORY_TYPE, 0700); + } + _instancePipe.append(Zotero.instanceID); + + Zotero.IPC.Pipe.initPipeListener(_instancePipe, this.parsePipeInput); + } + } + + /** + * Parses input received via instance pipe + */ + this.parsePipeInput = function(msg) { + // remove a newline if there is one + if(msg[msg.length-1] === "\n") msg = msg.substr(0, msg.length-1); + + Zotero.debug('IPC: Received "'+msg+'"'); + + if(msg === "releaseLock" && !Zotero.isConnector) { + switchConnectorMode(true); + } else if(msg === "lockReleased") { + Zotero.onDBLockReleased(); + } else if(msg === "initComplete") { + Zotero.onInitComplete(); + } + } + + /** + * Broadcast a message to all other Zotero instances + */ + this.broadcast = function(msg) { + if(Zotero.isWin) { // communicate via WM_COPYDATA method + // there is no ctypes struct support in Fx 3.6 + // while we could mimic it, it's easier just to require users to upgrade if they + // want connector sharing + if(!Zotero.isFx4) return false; + + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + // communicate via message window + var user32 = ctypes.open("user32.dll"); + + /* http://msdn.microsoft.com/en-us/library/ms633499%28v=vs.85%29.aspx + * HWND WINAPI FindWindow( + * __in_opt LPCTSTR lpClassName, + * __in_opt LPCTSTR lpWindowName + * ); + */ + var FindWindow = user32.declare("FindWindowW", ctypes.winapi_abi, ctypes.int32_t, + ctypes.jschar.ptr, ctypes.jschar.ptr); + + /* http://msdn.microsoft.com/en-us/library/ms633539%28v=vs.85%29.aspx + * BOOL WINAPI SetForegroundWindow( + * __in HWND hWnd + * ); + */ + var SetForegroundWindow = user32.declare("SetForegroundWindow", ctypes.winapi_abi, + ctypes.bool, ctypes.int32_t); + + /* + * LRESULT WINAPI SendMessage( + * __in HWND hWnd, + * __in UINT Msg, + * __in WPARAM wParam, + * __in LPARAM lParam + * ); + */ + var SendMessage = user32.declare("SendMessageW", ctypes.winapi_abi, ctypes.uintptr_t, + ctypes.int32_t, ctypes.unsigned_int, ctypes.voidptr_t, ctypes.voidptr_t); + + /* http://msdn.microsoft.com/en-us/library/ms649010%28v=vs.85%29.aspx + * typedef struct tagCOPYDATASTRUCT { + * ULONG_PTR dwData; + * DWORD cbData; + * PVOID lpData; + * } COPYDATASTRUCT, *PCOPYDATASTRUCT; + */ + var COPYDATASTRUCT = ctypes.StructType("COPYDATASTRUCT", [ + {"dwData":ctypes.voidptr_t}, + {"cbData":ctypes.uint32_t}, + {"lpData":ctypes.voidptr_t} + ]); + + const appNames = ["Firefox", "Zotero", "Nightly", "Aurora", "Minefield"]; + for each(var appName in appNames) { + // don't send messages to ourself + if(appName === Zotero.appName) continue; + + var thWnd = FindWindow(appName+"MessageWindow", null); + if(thWnd) { + Zotero.debug('IPC: Broadcasting "'+msg+'" to window "'+appName+'MessageWindow"'); + + // allocate message + var data = ctypes.char.array()('firefox.exe -ZoteroIPC "'+msg.replace('"', '""', "g")+'"\x00C:\\'); + var dataSize = data.length*data.constructor.size; + + // create new COPYDATASTRUCT + var cds = new COPYDATASTRUCT(); + cds.dwData = null; + cds.cbData = dataSize; + cds.lpData = data.address(); + + // send COPYDATASTRUCT + var success = SendMessage(thWnd, 0x004A /** WM_COPYDATA **/, null, cds.address()); + + user32.close(); + return !!success; + } + } + + user32.close(); + return false; + } else { // communicate via pipes + + // look for other Zotero instances + var pipes = []; + var pipeDir = _getPipeDirectory(); + if(pipeDir.exists()) { + var dirEntries = pipeDir.directoryEntries; + while (dirEntries.hasMoreElements()) { + var pipe = dirEntries.getNext().QueryInterface(Ci.nsILocalFile); + if(pipe.leafName[0] !== "." && (!_instancePipe || !pipe.equals(_instancePipe))) { + pipes.push(pipe); + } + } + } + + if(!pipes.length) return false; + + // safely write to instance pipes + var lib = this.getLibc(); + if(!lib) return false; + + // int open(const char *path, int oflag); + if(Zotero.isFx36) { + var open = lib.declare("open", ctypes.default_abi, ctypes.int32_t, ctypes.string, ctypes.int32_t); + } else { + var open = lib.declare("open", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int); + } + // ssize_t write(int fildes, const void *buf, size_t nbyte); + if(Zotero.isFx36) { + } else { + var write = lib.declare("write", ctypes.default_abi, ctypes.ssize_t, ctypes.int, ctypes.char.ptr, ctypes.size_t); + } + // int close(int filedes); + if(Zotero.isFx36) { + } else { + var close = lib.declare("close", ctypes.default_abi, ctypes.int, ctypes.int); + } + + var success = false; + for each(var pipe in pipes) { + var fd = open(pipe.path, 0x0004 | 0x0001); // O_NONBLOCK | O_WRONLY + if(fd !== -1) { + Zotero.debug('IPC: Broadcasting "'+msg+'" to instance '+pipe.leafName); + success = true; + write(fd, msg+"\n", msg.length); + close(fd); + } else { + try { + pipe.remove(true); + } catch(e) {}; + } + } + + return success; + } + } + + /** + * Get directory containing Zotero pipes + */ + function _getPipeDirectory() { + var dir = Zotero.getZoteroDirectory(); + dir.append("pipes"); + return dir; + } + + /** + * Gets the path to libc as a string + */ + this.getLibcPath = function() { + if(_libcPath) return _libcPath; + + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + // get possible names for libc + if(Zotero.isMac) { + var possibleLibcs = ["/usr/lib/libc.dylib"]; + } else { + var possibleLibcs = [ + "libc.so.6", + "libc.so.6.1", + "libc.so" + ]; + } + + // try all possibilities + while(possibleLibcs.length) { + var libPath = possibleLibcs.shift(); + try { + var lib = ctypes.open(libPath); + break; + } catch(e) {} + } + + // throw appropriate error on failure + if(!lib) { + Components.utils.reportError("Zotero: libc could not be loaded. Word processor integration "+ + "and other functionality will not be available. Please post on the Zotero Forums so we "+ + "can add support for your operating system."); + return; + } + + _libc = lib; + _libcPath = libPath; + return libPath; + } + + /** + * Gets standard C library via ctypes + */ + this.getLibc = function() { + if(!_libc) this.getLibcPath(); + return _libc; + } +} + +/** + * Methods for reading from and writing to a pipe + */ +Zotero.IPC.Pipe = new function() { + var _mkfifo, _pipeClass; + + /** + * Creates and listens on a pipe + * + * @param {nsIFile} file The location where the pipe should be created + * @param {Function} callback A function to be passed any data recevied on the pipe + */ + this.initPipeListener = function(file, callback) { + Zotero.debug("IPC: Initializing pipe at "+file.path); + + // determine type of pipe + if(!_pipeClass) { + var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"] + .getService(Components.interfaces.nsIVersionComparator); + var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]. + getService(Components.interfaces.nsIXULAppInfo); + if(verComp.compare("2.2a1pre", appInfo.platformVersion) <= 0) { // Gecko 5 + _pipeClass = Zotero.IPC.Pipe.DeferredOpen; + } else if(verComp.compare("2.0b9pre", appInfo.platformVersion) <= 0) { // Gecko 2.0b9+ + _pipeClass = Zotero.IPC.Pipe.WorkerThread; + } else { // Gecko 1.9.2 + _pipeClass = Zotero.IPC.Pipe.Poll; + } + } + + // make new pipe + new _pipeClass(file, callback); + } + + /** + * Makes a fifo + * @param {nsIFile} file Location to create the fifo + */ + this.mkfifo = function(file) { + // int mkfifo(const char *path, mode_t mode); + if(!_mkfifo) { + var libc = Zotero.IPC.getLibc(); + if(!libc) return false; + if(Zotero.isFx36) { + _mkfifo = libc.declare("mkfifo", ctypes.default_abi, ctypes.int32_t, ctypes.string, ctypes.uint32_t); + } else { + _mkfifo = libc.declare("mkfifo", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.unsigned_int); + } + } + + // make pipe + var ret = _mkfifo(file.path, 0600); + return file.exists(); + } + + /** + * Adds a shutdown listener for a pipe that writes "Zotero shutdown\n" to the pipe and then + * deletes it + */ + this.writeShutdownMessage = function(file) { + var oStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. + getService(Components.interfaces.nsIFileOutputStream); + oStream.init(file, 0x02 | 0x10, 0, 0); + const cmd = "Zotero shutdown\n"; + oStream.write(cmd, cmd.length); + oStream.close(); + file.remove(false); + Zotero.debug("IPC: Closing pipe "+file.path); + } +} + +/** + * Listens asynchronously for data on the integration pipe and reads it when available + * + * Used to read from pipe on Gecko 5+ + */ +Zotero.IPC.Pipe.DeferredOpen = function(file, callback) { + this._file = file; + this._callback = callback; + + if(!Zotero.IPC.Pipe.mkfifo(file)) return; + + this._initPump(); + + // add shutdown listener + Zotero.addShutdownListener(Zotero.IPC.Pipe.writeShutdownMessage.bind(null, file)); +} + +Zotero.IPC.Pipe.DeferredOpen.prototype = { + "onStartRequest":function() {}, + "onStopRequest":function() {}, + "onDataAvailable":function(request, context, inputStream, offset, count) { + // read from pipe + var converterInputStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Components.interfaces.nsIConverterInputStream); + converterInputStream.init(inputStream, "UTF-8", 4096, + Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + var out = {}; + converterInputStream.readString(count, out); + inputStream.close(); + + if(out.value === "Zotero shutdown\n") return + + this._initPump(); + this._callback(out.value); + }, + + /** + * Initializes the nsIInputStream and nsIInputStreamPump to read from _fifoFile + * + * Used after reading from file on Gecko 5+ + */ + "_initPump":function() { + var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream); + // 16 = open as deferred so that we don't block on open + fifoStream.init(this._file, -1, 0, 16); + + var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]. + createInstance(Components.interfaces.nsIInputStreamPump); + pump.init(fifoStream, -1, -1, 4096, 1, true); + pump.asyncRead(this, null); + } +}; + +/** + * Listens synchronously for data on the integration pipe on a separate JS thread and reads it + * when available + * + * Used to read from pipe on Gecko 2 + */ +Zotero.IPC.Pipe.WorkerThread = function(file, callback) { + this._callback = callback; + + if(!Zotero.IPC.Pipe.mkfifo(file)) return; + + // set up worker + var worker = Components.classes["@mozilla.org/threads/workerfactory;1"] + .createInstance(Components.interfaces.nsIWorkerFactory) + .newChromeWorker("chrome://zotero/content/xpcom/pipe_worker.js"); + worker.onmessage = this.onmessage.bind(this); + worker.postMessage({"path":file.path, "libc":Zotero.IPC.getLibcPath()}); + + // add shutdown listener + Zotero.addShutdownListener(Zotero.IPC.Pipe.writeShutdownMessage.bind(null, file)); +} + +Zotero.IPC.Pipe.WorkerThread.prototype = { + /** + * onmessage call for worker thread, to get data from it + */ + "onmessage":function(event) { + if(event.data[0] === "Exception") { + throw event.data[1]; + } else if(event.data[0] === "Debug") { + Zotero.debug(event.data[1]); + } else if(event.data[0] === "Read") { + this._callback(event.data[1]); + } + } +} + +/** + * Polling mechanism for file + * + * Used to read from integration "pipe" on Gecko 1.9.2/Firefox 3.6 + */ +Zotero.IPC.Pipe.Poll = function(file, callback) { + this._file = file; + this._callback = callback; + + // create empty file + this._clearFile(); + + // no deferred open capability, so we need to poll + this._timer = Components.classes["@mozilla.org/timer;1"]. + createInstance(Components.interfaces.nsITimer); + this._timer.initWithCallback(this, 1000, + Components.interfaces.nsITimer.TYPE_REPEATING_SLACK); + + // this has to be in global scope so we don't get garbage collected + Zotero.IPC.Pipe.Poll._activePipes.push(this); + + // add shutdown listener + Zotero.addShutdownListener(this); +} +Zotero.IPC.Pipe.Poll._activePipes = []; + +Zotero.IPC.Pipe.Poll.prototype = { + /** + * Called every second to check if there is new data to be read + */ + "notify":function() { + if(this._file.fileSize === 0) return; + + // read from pipe (file, actually) + var string = Zotero.File.getContents(this._file); + this._clearFile(); + + // run command + this._callback(string); + }, + + /** + * Called on quit to remove the file + */ + "observe":function() { + this._file.remove(); + }, + + /** + * Clears the old contents of the fifo file + */ + "_clearFile":function() { + // clear file + var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. + createInstance(Components.interfaces.nsIFileOutputStream); + foStream.init(_fifoFile, 0x02 | 0x08 | 0x20, 0666, 0); + foStream.close(); + } +}; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/mimeTypeHandler.js b/chrome/content/zotero/xpcom/mimeTypeHandler.js index c477e3d3e..c0b5819bd 100644 --- a/chrome/content/zotero/xpcom/mimeTypeHandler.js +++ b/chrome/content/zotero/xpcom/mimeTypeHandler.js @@ -180,6 +180,8 @@ Zotero.MIMETypeHandler = new function () { */ var _Observer = new function() { this.observe = function(channel) { + if(Zotero.isConnector) return; + channel.QueryInterface(Components.interfaces.nsIRequest); if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) { channel.QueryInterface(Components.interfaces.nsIHttpChannel); @@ -222,6 +224,7 @@ Zotero.MIMETypeHandler = new function () { * Called to see if we can handle a content type */ this.canHandleContent = this.isPreferred = function(contentType, isContentPreferred, desiredContentType) { + if(Zotero.isConnector) return false; return !!_typeHandlers[contentType.toLowerCase()]; } diff --git a/chrome/content/zotero/xpcom/integration_worker.js b/chrome/content/zotero/xpcom/pipe_worker.js similarity index 80% rename from chrome/content/zotero/xpcom/integration_worker.js rename to chrome/content/zotero/xpcom/pipe_worker.js index bdaee72cb..8b317e929 100644 --- a/chrome/content/zotero/xpcom/integration_worker.js +++ b/chrome/content/zotero/xpcom/pipe_worker.js @@ -54,19 +54,12 @@ onmessage = function(event) { // extract message var string = buf.readString(); - var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/); - if(!parts) { - postMessage(["Exception", "Integration Worker: Invalid input received: "+string]); - continue; - } - var agent = parts[1].toString(); - var cmd = parts[2].toString(); - var document = parts[3] ? parts[3] : null; - if(agent == "Zotero" && cmd == "shutdown") { - postMessage(["Debug", "Integration Worker: Shutting down"]); + if(string === "Zotero shutdown\n") { + postMessage(["Debug", "IPC: Worker closing "+event.data.path]); lib.close(); return; } - postMessage([agent, cmd, document]); + + postMessage(["Read", string]); } }; \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/proxy.js b/chrome/content/zotero/xpcom/proxy.js index b684362a5..075d4d695 100644 --- a/chrome/content/zotero/xpcom/proxy.js +++ b/chrome/content/zotero/xpcom/proxy.js @@ -480,8 +480,6 @@ const Zotero_Proxy_schemeParameterRegexps = { "%a":/([^%])%a/ }; -const Zotero_Proxy_metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g; - /** * Compiles the regular expression against which we match URLs to determine if this proxy is in use * and saves it in this.regexp @@ -514,7 +512,7 @@ Zotero.Proxy.prototype.compileRegexp = function() { }) // now replace with regexp fragment in reverse order - var re = "^"+this.scheme.replace(Zotero_Proxy_metaRegexp, "\\$&")+"$"; + var re = "^"+Zotero.Utilities.quotemeta(this.scheme)+"$"; for(var i=this.parameters.length-1; i>=0; i--) { var param = this.parameters[i]; re = re.replace(Zotero_Proxy_schemeParameterRegexps[param], "$1"+parametersToCheck[param]); @@ -571,7 +569,7 @@ Zotero.Proxy.prototype.save = function(transparent) { if(hasErrors) throw "Zotero.Proxy: could not be saved because it is invalid: error "+hasErrors[0]; // we never save any changes to non-persisting proxies, so this works - var newProxy = !!this.proxyID; + var newProxy = !this.proxyID; this.autoAssociate = this.multiHost && this.autoAssociate; this.compileRegexp(); diff --git a/chrome/content/zotero/xpcom/server.js b/chrome/content/zotero/xpcom/server.js new file mode 100755 index 000000000..ad1112bda --- /dev/null +++ b/chrome/content/zotero/xpcom/server.js @@ -0,0 +1,379 @@ +/* + ***** 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Server = new function() { + var _onlineObserverRegistered; + var responseCodes = { + 200:"OK", + 201:"Created", + 300:"Multiple Choices", + 400:"Bad Request", + 404:"Not Found", + 500:"Internal Server Error", + 501:"Method Not Implemented" + }; + + /** + * initializes a very rudimentary web server + */ + this.init = function() { + if (Zotero.HTTP.browserIsOffline()) { + Zotero.debug('Browser is offline -- not initializing 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('httpServer.port'), true, -1); + serv.asyncListen(Zotero.Server.SocketListener); + + Zotero.debug("HTTP server listening on 127.0.0.1:"+serv.port); + } catch(e) { + Zotero.debug("Not initializing HTTP server"); + } + + _registerOnlineObserver() + } + + /** + * generates the response to an HTTP request + */ + this.generateResponse = function (status, contentType, body) { + var response = "HTTP/1.0 "+status+" "+responseCodes[status]+"\r\n"; + response += "Access-Control-Allow-Origin: org.zotero.zoteroconnectorforsafari-69x6c999f9\r\n"; + response += "Access-Control-Allow-Methods: POST, GET, OPTIONS, HEAD\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.Server.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.Server.SocketListener = new function() { + this.onSocketAccepted = onSocketAccepted; + this.onStopListening = onStopListening; + + /* + * called when a socket is opened + */ + function onSocketAccepted(socket, transport) { + // get an input stream + var iStream = transport.openInputStream(0, 0, 0); + var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0); + + var dataListener = new Zotero.Server.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("HTTP server going offline"); + } +} + +/* + * handles the actual acquisition of data + */ +Zotero.Server.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.Server.DataListener.prototype.onStartRequest = function(request, context) {} + +/* + * called when a request stops + */ +Zotero.Server.DataListener.prototype.onStopRequest = function(request, context, status) { + this.iStream.close(); + this.oStream.close(); +} + +/* + * called when new data is available + */ +Zotero.Server.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.Server.DataListener.prototype._headerFinished = function() { + this.headerFinished = true; + + Zotero.debug(this.header); + + const methodRe = /^([A-Z]+) ([^ \r\n?]+)(\?[^ \r\n]+)?/; + const contentTypeRe = /[\r\n]Content-Type: +([^ \r\n]+)/i; + + // get first line of request + var method = methodRe.exec(this.header); + // get content-type + var contentType = contentTypeRe.exec(this.header); + if(contentType) { + var splitContentType = contentType[1].split(/\s*;/); + this.contentType = splitContentType[0]; + } + + if(!method) { + this._requestFinished(Zotero.Server.generateResponse(400)); + return; + } + if(!Zotero.Server.Endpoints[method[2]]) { + this._requestFinished(Zotero.Server.generateResponse(404)); + return; + } + this.endpoint = Zotero.Server.Endpoints[method[2]]; + + if(method[1] == "HEAD" || method[1] == "OPTIONS") { + this._requestFinished(Zotero.Server.generateResponse(200)); + } else if(method[1] == "GET") { + this._requestFinished(this._processEndpoint("GET", method[3])); + } else if(method[1] == "POST") { + const contentLengthRe = /[\r\n]Content-Length: +([0-9]+)/i; + + // parse content length + var m = contentLengthRe.exec(this.header); + if(!m) { + this._requestFinished(Zotero.Server.generateResponse(400)); + return; + } + + this.bodyLength = parseInt(m[1]); + this._bodyData(); + } else { + this._requestFinished(Zotero.Server.generateResponse(501)); + return; + } +} + +/* + * checks to see if Content-Length bytes of body have been read and, if so, processes the body + */ +Zotero.Server.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 + this._processEndpoint("POST", this.body); + } +} + +/** + * Generates a response based on calling the function associated with the endpoint + */ +Zotero.Server.DataListener.prototype._processEndpoint = function(method, postData) { + try { + var endpoint = new this.endpoint; + + // check that endpoint supports method + if(endpoint.supportedMethods.indexOf(method) === -1) { + this._requestFinished(Zotero.Server.generateResponse(400)); + return; + } + + var decodedData = null; + if(postData && this.contentType) { + // check that endpoint supports contentType + if(endpoint.supportedDataTypes.indexOf(this.contentType) === -1) { + this._requestFinished(Zotero.Server.generateResponse(400)); + return; + } + + // decode JSON or urlencoded post data, and pass through anything else + if(this.contentType === "application/json") { + decodedData = JSON.parse(postData); + } else if(this.contentType === "application/x-www-urlencoded") { + var splitData = postData.split("&"); + decodedData = {}; + for each(var variable in splitData) { + var splitIndex = variable.indexOf("="); + data[decodeURIComponent(variable.substr(0, splitIndex))] = decodeURIComponent(variable.substr(splitIndex+1)); + } + } else { + decodedData = postData; + } + } + + // set up response callback + var me = this; + var sendResponseCallback = function(code, contentType, arg) { + me._requestFinished(Zotero.Server.generateResponse(code, contentType, arg)); + } + + // pass to endpoint + endpoint.init(decodedData, sendResponseCallback); + } catch(e) { + Zotero.debug(e); + this._requestFinished(Zotero.Server.generateResponse(500)); + throw e; + } +} + +/* + * returns HTTP data from a request + */ +Zotero.Server.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 + Zotero.debug(response); + intlStream.writeString(response); + } finally { + intlStream.close(); + } +} + + +/** + * Endpoints for the HTTP server + * + * Each endpoint should take the form of an object. The init() method of this object will be passed: + * method - the method of the request ("GET" or "POST") + * data - the query string (for a "GET" request) or POST data (for a "POST" request) + * sendResponseCallback - a function to send a response to the HTTP request. This can be passed + * a response code alone (e.g., sendResponseCallback(404)) or a response + * code, MIME type, and response body + * (e.g., sendResponseCallback(200, "text/plain", "Hello World!")) + * + * See connector/server_connector.js for examples + */ +Zotero.Server.Endpoints = {} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js new file mode 100755 index 000000000..5b878de2c --- /dev/null +++ b/chrome/content/zotero/xpcom/server_connector.js @@ -0,0 +1,607 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see . + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Server.Connector = function() {}; +Zotero.Server.Connector._waitingForSelection = {}; +Zotero.Server.Connector.Data = {}; + +/** + * Manage cookies in a sandboxed fashion + * + * @param {browser} browser Hidden browser object + * @param {String} uri URI of page to manage cookies for (cookies for domains that are not + * subdomains of this URI are ignored) + * @param {String} cookieData Cookies with which to initiate the sandbox + */ +Zotero.Server.Connector.CookieManager = function(browser, uri, cookieData) { + this._webNav = browser.webNavigation; + this._browser = browser; + this._watchedBrowsers = [browser]; + this._observerService = Components.classes["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService); + + this._uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(uri, null, null); + + var splitCookies = cookieData.split(/; ?/); + this._cookies = {}; + for each(var cookie in splitCookies) { + var key = cookie.substr(0, cookie.indexOf("=")); + var value = cookie.substr(cookie.indexOf("=")+1); + this._cookies[key] = value; + } + + [this._observerService.addObserver(this, topic, false) for each(topic in this._observerTopics)]; +} + +Zotero.Server.Connector.CookieManager.prototype = { + "_observerTopics":["http-on-examine-response", "http-on-modify-request", "quit-application"], + "_watchedXHRs":[], + + /** + * nsIObserver implementation for adding, clearing, and slurping cookies + */ + "observe": function(channel, topic) { + if(topic == "quit-application") { + Zotero.debug("WARNING: A Zotero.Server.CookieManager for "+this._uri.spec+" was still open on shutdown"); + } else { + channel.QueryInterface(Components.interfaces.nsIHttpChannel); + var isTracked = null; + try { + var topDoc = channel.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document; + for each(var browser in this._watchedBrowsers) { + isTracked = topDoc == browser.contentDocument; + if(isTracked) break; + } + } catch(e) {} + if(isTracked === null) { + try { + isTracked = channel.loadGroup.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document == this._browser.contentDocument; + } catch(e) {} + } + if(isTracked === null) { + try { + isTracked = this._watchedXHRs.indexOf(channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest)) !== -1; + } catch(e) {} + } + + // isTracked is now either true, false, or null + // true => we should manage cookies for this request + // false => we should not manage cookies for this request + // null => this request is of a type we couldn't match to this request. one such type + // is a link prefetch (nsPrefetchNode) but there might be others as well. for + // now, we are paranoid and reject these. + + if(isTracked === false) { + Zotero.debug("Zotero.Server.CookieManager: not touching channel for "+channel.URI.spec); + return; + } else if(isTracked) { + Zotero.debug("Zotero.Server.CookieManager: managing cookies for "+channel.URI.spec); + } else { + Zotero.debug("Zotero.Server.CookieManager: being paranoid about channel for "+channel.URI.spec); + } + + if(topic == "http-on-modify-request") { + // clear cookies to be sent to other domains + if(isTracked === null || channel.URI.host != this._uri.host) { + channel.setRequestHeader("Cookie", "", false); + channel.setRequestHeader("Cookie2", "", false); + Zotero.debug("Zotero.Server.CookieManager: cleared cookies to be sent to "+channel.URI.spec); + return; + } + + // add cookies to be sent to this domain + var cookies = [key+"="+this._cookies[key] + for(key in this._cookies)].join("; "); + channel.setRequestHeader("Cookie", cookies, false); + Zotero.debug("Zotero.Server.CookieManager: added cookies for request to "+channel.URI.spec); + } else if(topic == "http-on-examine-response") { + // clear cookies being received + try { + var cookieHeader = channel.getResponseHeader("Set-Cookie"); + } catch(e) { + return; + } + channel.setResponseHeader("Set-Cookie", "", false); + channel.setResponseHeader("Set-Cookie2", "", false); + + // don't process further if these cookies are for another set of domains + if(isTracked === null || channel.URI.host != this._uri.host) { + Zotero.debug("Zotero.Server.CookieManager: rejected cookies from "+channel.URI.spec); + return; + } + + // put new cookies into our sandbox + if(cookieHeader) { + var cookies = cookieHeader.split(/; ?/); + var newCookies = {}; + for each(var cookie in cookies) { + var key = cookie.substr(0, cookie.indexOf("=")); + var value = cookie.substr(cookie.indexOf("=")+1); + var lcCookie = key.toLowerCase(); + + if(["comment", "domain", "max-age", "path", "version", "expires"].indexOf(lcCookie) != -1) { + // ignore cookie parameters; we are only holding cookies for a few minutes + // with a single domain, and the path attribute doesn't allow any additional + // security. + // DEBUG: does ignoring the path attribute break any sites? + continue; + } else if(lcCookie == "secure") { + // don't accept secure cookies + newCookies = {}; + break; + } else { + newCookies[key] = value; + } + } + [this._cookies[key] = newCookies[key] for(key in newCookies)]; + } + + Zotero.debug("Zotero.Server.CookieManager: slurped cookies from "+channel.URI.spec); + } + } + }, + + /** + * Attach CookieManager to a specific XMLHttpRequest + * @param {XMLHttpRequest} xhr + */ + "attachToBrowser": function(browser) { + this._watchedBrowsers.push(browser); + }, + + /** + * Attach CookieManager to a specific XMLHttpRequest + * @param {XMLHttpRequest} xhr + */ + "attachToXHR": function(xhr) { + this._watchedXHRs.push(xhr); + }, + + /** + * Destroys this CookieManager (intended to be executed when the browser is destroyed) + */ + "destroy": function() { + [this._observerService.removeObserver(this, topic) for each(topic in this._observerTopics)]; + } +} + + +/** + * Lists all available translators, including code for translators that should be run on every page + * + * Accepts: + * browser - one-letter code of the current browser + * g = Gecko (Firefox) + * c = Google Chrome (WebKit & V8) + * s = Safari (WebKit & Nitro/Squirrelfish Extreme) + * i = Internet Explorer + * Returns: + * translators - Zotero.Translator objects + * schema - Some information about the database. Currently includes: + * itemTypes + * name + * localizedString + * creatorTypes + * fields + * baseFields + * creatorTypes + * name + * localizedString + * fields + * name + * localizedString + */ +Zotero.Server.Connector.GetData = function() {}; +Zotero.Server.Endpoints["/connector/getData"] = Zotero.Server.Connector.GetData; +Zotero.Server.Connector.GetData.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Gets available translator list and other important data + * @param {Object} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + // Translator data + var responseData = {"preferences":{}, "translators":[]}; + + // TODO only send necessary translators + var translators = Zotero.Translators.getAll(); + for each(var translator in translators) { + let serializableTranslator = {}; + for each(var key in ["translatorID", "translatorType", "label", "creator", "target", + "priority", "browserSupport"]) { + serializableTranslator[key] = translator[key]; + } + + // Do not pass targetless translators that do not support this browser (since that + // would mean passing each page back to Zotero) + responseData.translators.push(serializableTranslator); + } + + // Various DB data (only sending what is required at the moment) + var systemVersion = Zotero.Schema.getDBVersion("system"); + if(systemVersion != data.systemVersion) { + responseData.schema = this._generateTypeSchema(); + } + + // Preferences + var prefs = Zotero.Prefs.prefBranch.getChildList("", {}, {}); + for each(var pref in prefs) { + responseData.preferences[pref] = Zotero.Prefs.get(pref); + } + + sendResponseCallback(200, "application/json", JSON.stringify(responseData)); + }, + + /** + * Generates a type schema. This is used by connector/type.js to handle types without DB access. + */ + "_generateTypeSchema":function() { + var schema = {"itemTypes":{}, "creatorTypes":{}, "fields":{}}; + var types = Zotero.ItemTypes.getTypes(); + + var fieldIDs = Zotero.DB.columnQuery("SELECT fieldID FROM fieldsCombined"); + var baseMappedFields = Zotero.ItemFields.getBaseMappedFields(); + for each(var fieldID in fieldIDs) { + var fieldObj = {"name":Zotero.ItemFields.getName(fieldID)}; + try { + fieldObj.localizedString = Zotero.getString("itemFields." + fieldObj.name) + } catch(e) {} + schema.fields[fieldID] = fieldObj; + } + + // names, localizedStrings, creatorTypes, and fields for each item type + for each(var type in types) { + var fieldIDs = Zotero.ItemFields.getItemTypeFields(type.id); + var baseFields = {}; + for each(var fieldID in fieldIDs) { + if(baseMappedFields.indexOf(fieldID) !== -1) { + baseFields[fieldID] = Zotero.ItemFields.getFieldIDFromTypeAndBase(type.id, fieldID); + } + } + + var icon = Zotero.ItemTypes.getImageSrc(type.name); + icon = icon.substr(icon.lastIndexOf("/")+1); + + schema.itemTypes[type.id] = {"name":type.name, + "localizedString":Zotero.ItemTypes.getLocalizedString(type.name), + "creatorTypes":[creatorType.id for each(creatorType in Zotero.CreatorTypes.getTypesForItemType(type.id))], + "fields":fieldIDs, "baseFields":baseFields, "icon":icon}; + + } + + var types = Zotero.CreatorTypes.getTypes(); + for each(var type in types) { + schema.creatorTypes[type.id] = {"name":type.name, + "localizedString":Zotero.CreatorTypes.getLocalizedString(type.name)}; + } + + return schema; + } +} + +/** + * Detects whether there is an available translator to handle a given page + * + * Accepts: + * uri - The URI of the page to be saved + * html - document.innerHTML or equivalent + * cookie - document.cookie or equivalent + * + * Returns a list of available translators as an array + */ +Zotero.Server.Connector.Detect = function() {}; +Zotero.Server.Endpoints["/connector/detect"] = Zotero.Server.Connector.Detect; +Zotero.Server.Connector.Data = {}; +Zotero.Server.Connector.Detect.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Loads HTML into a hidden browser and initiates translator detection + * @param {Object} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + this._sendResponse = sendResponseCallback; + this._parsedPostData = data; + + this._translate = new Zotero.Translate("web"); + this._translate.setHandler("translators", function(obj, item) { me._translatorsAvailable(obj, item) }); + + Zotero.Server.Connector.Data[this._parsedPostData["uri"]] = ""+this._parsedPostData["html"]+""; + this._browser = Zotero.Browser.createHiddenBrowser(); + + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var uri = ioService.newURI(this._parsedPostData["uri"], "UTF-8", null); + + var pageShowCalled = false; + var me = this; + this._translate.setCookieManager(new Zotero.Server.Connector.CookieManager(this._browser, + this._parsedPostData["uri"], this._parsedPostData["cookie"])); + this._browser.addEventListener("DOMContentLoaded", function() { + try { + if(me._browser.contentDocument.location.href == "about:blank") return; + if(pageShowCalled) return; + pageShowCalled = true; + delete Zotero.Server.Connector.Data[me._parsedPostData["uri"]]; + + // get translators + me._translate.setDocument(me._browser.contentDocument); + me._translate.getTranslators(); + } catch(e) { + Zotero.debug(e); + throw e; + } + }, false); + + me._browser.loadURI("zotero://connector/"+encodeURIComponent(this._parsedPostData["uri"])); + }, + + /** + * Callback to be executed when list of translators becomes available. Sends response with + * item types, translator IDs, labels, and icons for available translators. + * @param {Zotero.Translate} translate + * @param {Zotero.Translator[]} translators + */ + "_translatorsAvailable":function(obj, translators) { + var jsons = []; + for each(var translator in translators) { + if(translator.itemType == "multiple") { + var icon = "treesource-collection.png" + } else { + var icon = Zotero.ItemTypes.getImageSrc(translator.itemType); + icon = icon.substr(icon.lastIndexOf("/")+1); + } + var json = {"itemType":translator.itemType, "translatorID":translator.translatorID, + "label":translator.label, "priority":translator.priority} + jsons.push(json); + } + this._sendResponse(200, "application/json", JSON.stringify(jsons)); + + this._translate.cookieManager.destroy(); + Zotero.Browser.deleteHiddenBrowser(this._browser); + } +} + +/** + * Performs translation of a given page + * + * Accepts: + * uri - The URI of the page to be saved + * html - document.innerHTML or equivalent + * cookie - document.cookie or equivalent + * + * Returns: + * If a single item, sends response code 201 with no body. + * If multiple items, sends response code 300 with the following content: + * items - list of items in the format typically passed to the selectItems handler + * instanceID - an ID that must be maintained for the subsequent Zotero.Connector.Select call + * uri - the URI of the page for which multiple items are available + */ +Zotero.Server.Connector.SavePage = function() {}; +Zotero.Server.Endpoints["/connector/savePage"] = Zotero.Server.Connector.SavePage; +Zotero.Server.Connector.SavePage.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Either loads HTML into a hidden browser and initiates translation, or saves items directly + * to the database + * @param {Object} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + this._sendResponse = sendResponseCallback; + Zotero.Server.Connector.Detect.prototype.init.apply(this, [data, sendResponseCallback]) + }, + + /** + * Callback to be executed when items must be selected + * @param {Zotero.Translate} translate + * @param {Object} itemList ID=>text pairs representing available items + */ + "_selectItems":function(translate, itemList, callback) { + var instanceID = Zotero.randomString(); + Zotero.Server.Connector._waitingForSelection[instanceID] = this; + + // Fix for translators that don't create item lists as objects + if(itemList.push && typeof itemList.push === "function") { + var newItemList = {}; + for(var item in itemList) { + newItemList[item] = itemList[item]; + } + itemList = newItemList; + } + + // Send "Multiple Choices" HTTP response + this._sendResponse(300, "application/json", JSON.stringify({"selectItems":itemList, "instanceID":instanceID, "uri":this._parsedPostData.uri})); + + // We need this to make sure that we won't stop Firefox from quitting, even if the user + // didn't close the selectItems window + var observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + var me = this; + var quitObserver = {observe:function() { me.selectedItems = false; }}; + observerService.addObserver(quitObserver, "quit-application", false); + + this.selectedItems = null; + var endTime = Date.now() + 60*60*1000; // after an hour, timeout, so that we don't + // permanently slow Firefox with this loop + while(this.selectedItems === null && Date.now() < endTime) { + Zotero.mainThread.processNextEvent(true); + } + + observerService.removeObserver(quitObserver, "quit-application"); + callback(this.selectedItems); + }, + + /** + * Callback to be executed when list of translators becomes available. Opens progress window, + * selects specified translator, and initiates translation. + * @param {Zotero.Translate} translate + * @param {Zotero.Translator[]} translators + */ + "_translatorsAvailable":function(translate, translators) { + // make sure translatorsAvailable succeded + if(!translators.length) { + Zotero.Browser.deleteHiddenBrowser(this._browser); + this._sendResponse(500); + return; + } + + // figure out where to save + var libraryID = null; + var collectionID = null; + var zp = Zotero.getActiveZoteroPane(); + try { + var libraryID = zp.getSelectedLibraryID(); + var collection = zp.getSelectedCollection(); + } catch(e) {} + + // set handlers for translation + var me = this; + var jsonItems = []; + translate.setHandler("select", function(obj, item, callback) { return me._selectItems(obj, item, callback) }); + translate.setHandler("itemDone", function(obj, item, jsonItem) { + if(collection) { + collection.addItem(item.id); + } + jsonItems.push(jsonItem); + }); + translate.setHandler("done", function(obj, item) { + me._translate.cookieManager.destroy(); + Zotero.Browser.deleteHiddenBrowser(me._browser); + me._sendResponse(201, "application/json", JSON.stringify({"items":jsonItems})); + }); + + // set translator and translate + translate.setTranslator(this._parsedPostData.translatorID); + translate.translate(libraryID); + } +} + +/** + * Performs translation of a given page, or, alternatively, saves items directly + * + * Accepts: + * items - an array of JSON format items + * Returns: + * 201 response code with empty body + */ +Zotero.Server.Connector.SaveItem = function() {}; +Zotero.Server.Endpoints["/connector/saveItems"] = Zotero.Server.Connector.SaveItem; +Zotero.Server.Connector.SaveItem.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Either loads HTML into a hidden browser and initiates translation, or saves items directly + * to the database + * @param {Object} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + // figure out where to save + var libraryID = null; + var collectionID = null; + var zp = Zotero.getActiveZoteroPane(); + try { + var libraryID = zp.getSelectedLibraryID(); + var collection = zp.getSelectedCollection(); + } catch(e) {} + + // save items + var itemSaver = new Zotero.Translate.ItemSaver(libraryID, + Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD, 1); + for each(var item in data.items) { + var savedItem = itemSaver.saveItem(item); + if(collection) collection.addItem(savedItem.id); + } + sendResponseCallback(201); + } +} + +/** + * Handle item selection + * + * Accepts: + * selectedItems - a list of items to translate in ID => text format as returned by a selectItems handler + * instanceID - as returned by savePage call + * Returns: + * 201 response code with empty body + */ +Zotero.Server.Connector.SelectItems = function() {}; +Zotero.Server.Endpoints["/connector/selectItems"] = Zotero.Server.Connector.SelectItems; +Zotero.Server.Connector.SelectItems.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Finishes up translation when item selection is complete + * @param {String} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(data, sendResponseCallback) { + var saveInstance = Zotero.Server.Connector._waitingForSelection[data.instanceID]; + saveInstance._sendResponse = sendResponseCallback; + + saveInstance.selectedItems = false; + for(var i in data.selectedItems) { + saveInstance.selectedItems = data.selectedItems; + break; + } + } +} + +/** + * Get code for a translator + * + * Accepts: + * translatorID + * Returns: + * code - translator code + */ +Zotero.Server.Connector.GetTranslatorCode = function() {}; +Zotero.Server.Endpoints["/connector/getTranslatorCode"] = Zotero.Server.Connector.GetTranslatorCode; +Zotero.Server.Connector.GetTranslatorCode.prototype = { + "supportedMethods":["POST"], + "supportedDataTypes":["application/json"], + + /** + * Finishes up translation when item selection is complete + * @param {String} data POST data or GET query string + * @param {Function} sendResponseCallback function to send HTTP response + */ + "init":function(postData, sendResponseCallback) { + var translator = Zotero.Translators.get(postData.translatorID); + sendResponseCallback(200, "application/javascript", translator.code); + } +} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/browser_other.js b/chrome/content/zotero/xpcom/translation/browser_other.js deleted file mode 100644 index c39d3531b..000000000 --- a/chrome/content/zotero/xpcom/translation/browser_other.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://zotero.org - - This file is part of Zotero. - - Zotero is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Zotero is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Zotero. If not, see . - - ***** END LICENSE BLOCK ***** -*/ - -/** - * @class Manages the translator sandbox - * @param {Zotero.Translate} translate - * @param {String|window} sandboxLocation - */ -Zotero.Translate.SandboxManager = function(translate, sandboxLocation) { - this.sandbox = {}; - this._translate = translate; -} - -Zotero.Translate.SandboxManager.prototype = { - /** - * Evaluates code in the sandbox - */ - "eval":function(code) { - // eval in sandbox scope - (new Function("with(this) { " + code + " }")).call(this.sandbox); - }, - - /** - * Imports an object into the sandbox - * - * @param {Object} object Object to be imported (under Zotero) - * @param {Boolean} passTranslateAsFirstArgument Whether the translate instance should be passed - * as the first argument to the function. - */ - "importObject":function(object, passAsFirstArgument) { - var translate = this._translate; - - for(var key in (object.__exposedProps__ ? object.__exposedProps__ : object)) { - var fn = (function(object, key) { return object[key] })(); - - // magic "this"-preserving wrapping closure - this.sandbox[key] = function() { - var args = (passAsFirstArgument ? [passAsFirstArgument] : []); - for(var i=0; i + *
* New code should use Zotero.Translate.Web, Zotero.Translate.Import, Zotero.Translate.Export, or * Zotero.Translate.Search */ Zotero.Translate = function(type) { Zotero.debug("Translate: WARNING: new Zotero.Translate() is deprecated; please don't use this if you don't have to"); // hack - var translate = Zotero.Translate.new(type); + var translate = Zotero.Translate.newInstance(type); for(var i in translate) { this[i] = translate[i]; } @@ -43,10 +44,14 @@ Zotero.Translate = function(type) { /** * Create a new translator by a string type */ -Zotero.Translate.new = function(type) { +Zotero.Translate.newInstance = function(type) { return new Zotero.Translate[type[0].toUpperCase()+type.substr(1).toLowerCase()]; } +/** + * Namespace for Zotero sandboxes + * @namespace + */ Zotero.Translate.Sandbox = { /** * Combines a sandbox with the base sandbox @@ -67,10 +72,11 @@ Zotero.Translate.Sandbox = { /** * Base sandbox. These methods are available to all translators. + * @namespace */ "Base": { /** - * Called as Zotero.Item#complete() from translators to save items to the database. + * Called as {@link Zotero.Item#complete} from translators to save items to the database. * @param {Zotero.Translate} translate * @param {SandboxItem} An item created using the Zotero.Item class from the sandbox */ @@ -86,7 +92,7 @@ Zotero.Translate.Sandbox = { // just return the item array if(translate._libraryID === false || translate._parentTranslator) { translate.newItems.push(item); - translate._runHandler("itemDone", item); + translate._runHandler("itemDone", item, item); return; } @@ -99,7 +105,8 @@ Zotero.Translate.Sandbox = { Zotero.wait(); } - translate._runHandler("itemDone", newItem); + // pass both the saved item and the original JS array item + translate._runHandler("itemDone", newItem, item); }, /** @@ -142,16 +149,19 @@ Zotero.Translate.Sandbox = { } Zotero.debug("Translate: creating translate instance of type "+type+" in sandbox"); - var translation = Zotero.Translate.new(type); + var translation = Zotero.Translate.newInstance(type); translation._parentTranslator = translate; if(translation instanceof Zotero.Translate.Export && !(translation instanceof Zotero.Translate.Export)) { throw("Translate: only export translators may call other export translators"); } - // for security reasons, safeTranslator wraps the translator object. - // note that setLocation() is not allowed - var safeTranslator = new Object(); + /** + * @class Wrapper for {@link Zotero.Translate} for safely calling another translator + * from inside an existing translator + * @inner + */ + var safeTranslator = {}; safeTranslator.__exposedProps__ = { "setSearch":"r", "setDocument":"r", @@ -185,43 +195,61 @@ Zotero.Translate.Sandbox = { ); }; safeTranslator.setString = function(arg) { translation.setString(arg) }; - safeTranslator.setTranslator = function(arg) { return translation.setTranslator(arg) }; + safeTranslator.setTranslator = function(arg) { + var success = translation.setTranslator(arg); + if(!success) { + throw "Translator "+translate.translator[0].translatorID+" attempted to call invalid translatorID "+arg; + } + }; safeTranslator.getTranslators = function() { return translation.getTranslators() }; safeTranslator.translate = function() { setDefaultHandlers(translate, translation); return translation.translate(false); }; + // TODO safeTranslator.getTranslatorObject = function(callback) { - translation._loadTranslator(translation.translator[0]); - - if(Zotero.isFx) { - // do same origin check - var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] - .getService(Components.interfaces.nsIScriptSecurityManager); - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); + var haveTranslatorFunction = function(translator) { + translation.translator[0] = translator; + if(!Zotero._loadTranslator(translator)) throw "Translator could not be loaded"; - var outerSandboxURI = ioService.newURI(typeof translate._sandboxLocation === "object" ? - translate._sandboxLocation.location : translate._sandboxLocation, null, null); - var innerSandboxURI = ioService.newURI(typeof translation._sandboxLocation === "object" ? - translation._sandboxLocation.location : translation._sandboxLocation, null, null); - Zotero.debug(outerSandboxURI.spec); - Zotero.debug(innerSandboxURI.spec); - - try { - secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false); - } catch(e) { - throw "Translate: getTranslatorObject() may not be called from web or search "+ - "translators to web or search translators from different origins."; + if(Zotero.isFx) { + // do same origin check + var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Components.interfaces.nsIScriptSecurityManager); + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + var outerSandboxURI = ioService.newURI(typeof translate._sandboxLocation === "object" ? + translate._sandboxLocation.location : translate._sandboxLocation, null, null); + var innerSandboxURI = ioService.newURI(typeof translation._sandboxLocation === "object" ? + translation._sandboxLocation.location : translation._sandboxLocation, null, null); + + try { + secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false); + } catch(e) { + throw "Translate: getTranslatorObject() may not be called from web or search "+ + "translators to web or search translators from different origins."; + } } + + translation._prepareTranslation(); + setDefaultHandlers(translate, translation); + + if(callback) callback(translation._sandboxManager.sandbox); + }; + + if(typeof translation.translator[0] === "object") { + haveTranslatorFunction(translation.translator[0]); + return translation._sandboxManager.sandbox; + } else { + if(Zotero.isConnector && !callback) { + throw "Translate: Translator must accept a callback to getTranslatorObject() to "+ + "operate in this translation environment."; + } + + Zotero.Translators.get(translation.translator[0], haveTranslatorFunction); + if(!Zotero.isConnector) return translation._sandboxManager.sandbox; } - - translation._prepareTranslation(); - setDefaultHandlers(translate, translation); - - // return sandbox - if(callback) callback(translation._sandboxManager.sandbox); - return translation._sandboxManager.sandbox; }; // TODO security is not super-tight here, as someone could pass something into arg @@ -275,22 +303,57 @@ Zotero.Translate.Sandbox = { /** * Lets user pick which items s/he wants to put in his/her library * @param {Zotero.Translate} translate - * @param {Object} options An set of id => name pairs in object format + * @param {Object} items An set of id => name pairs in object format */ - "selectItems":function(translate, options, callback) { - // hack to see if there are options - var haveOptions = false; - for(var i in options) { - haveOptions = true; - break; - } - - if(!haveOptions) { + "selectItems":function(translate, items, callback) { + if(Zotero.Utilities.isEmpty(items)) { throw "Translate: translator called select items with no items"; } - if(translate._handlers.select) { - options = translate._runHandler("select", options); + if(translate._selectedItems) { + // if we have a set of selected items for this translation, use them + return translate._selectedItems; + } else if(translate._handlers.select) { + var haveAsyncCallback = !!callback; + var haveAsyncHandler = false; + var returnedItems = null; + + // if this translator doesn't provide an async callback for selectItems, set things + // up so that we can wait to see if the select handler returns synchronously. If it + // doesn't, we will need to restart translation. + if(!haveAsyncCallback) { + callback = function(selectedItems) { + if(haveAsyncHandler) { + translate.translate(this._libraryID, this._saveAttachments, selectedItems); + } else { + returnedItems = selectedItems; + } + }; + } + + translate._runHandler("select", items, callback); + + if(!haveAsyncCallback) { + if(translate.translator[0].browserSupport !== "g") { + Zotero.debug("Translate: WARNING: This translator is configured for "+ + "non-Firefox browser support, but no callback was provided for "+ + "selectItems(). When executed outside of Firefox, a selectItems() call "+ + "will require that this translator to be called multiple times.", 3); + } + + if(returnedItems === null) { + // The select handler is asynchronous, but this translator doesn't support + // asynchronous select. We return false to abort translation in this + // instance, and we will restart it later when the selectItems call is + // complete. + haveAsyncHandler = true; + return false; + } else { + return returnedItems; + } + } + } else { // no handler defined; assume they want all of them + return options; } if(callback) callback(options); @@ -378,7 +441,7 @@ Zotero.Translate.Sandbox = { "Import":{ /** * Saves a collection to the DB - * Called as Zotero.Collection#complete() from the sandbox + * Called as {@link Zotero.Collection#complete} from the sandbox * @param {Zotero.Translate} translate * @param {SandboxCollection} collection */ @@ -520,7 +583,7 @@ Zotero.Translate.Base.prototype = { throw("No translatorID specified"); } } else { - this.translator = [Zotero.Translators.get(translator)]; + this.translator = [translator]; } return !!this.translator; @@ -591,17 +654,25 @@ Zotero.Translate.Base.prototype = { * @param {String} type See {@link Zotero.Translate.Base#setHandler} for valid values * @param {Any} argument Argument to be passed to handler */ - "_runHandler":function(type, argument) { + "_runHandler":function(type) { var returnValue = undefined; if(this._handlers[type]) { + // compile list of arguments + if(this._parentTranslator) { + // if there is a parent translator, make sure we don't the Zotero.Translate + // object, since it could open a security hole + var args = [null]; + } else { + var args = [this]; + } + for(var i=1; i www.nature.com) + var m = /^(https?:\/\/)([^\/]+)/i.exec(uri); + if(m) { + var hostnames = m[2].split("."); + for(var i=1; i= 2) { + Components.utils.import("resource://gre/modules/AddonManager.jsm"); +} + /* * Core functions */ -var Zotero = new function(){ + (function(){ // Privileged (public) methods this.init = init; this.stateCheck = stateCheck; @@ -173,34 +182,39 @@ var Zotero = new function(){ var _locked; var _unlockCallbacks = []; + var _shutdownListeners = []; var _progressMeters; var _lastPercentage; + // whether we are waiting for another Zotero process to release its DB lock + var _waitingForDBLock = false; + // whether we are waiting for another Zotero process to initialize so we can use connector + var _waitingForInitComplete = false; + + // whether we should broadcast an initComplete message when initialization finishes (we should + // do this if we forced another Zotero process to release its lock) + var _broadcastInitComplete = false; + /** * A set of nsITimerCallbacks to be executed when Zotero.wait() completes */ var _waitTimerCallbacks = []; - /* + /** * Initialize the extension */ - function init(){ + function init() { if (this.initialized || this.skipLoading) { return false; } - var start = (new Date()).getTime() + var start = (new Date()).getTime(); - // Register shutdown handler to call Zotero.shutdown() var observerService = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); - observerService.addObserver({ - observe: Zotero.shutdown - }, "quit-application", false); // Load in the preferences branch for the extension Zotero.Prefs.init(); - Zotero.Debug.init(); this.mainThread = Components.classes["@mozilla.org/thread-manager;1"].getService().mainThread; @@ -214,6 +228,7 @@ var Zotero = new function(){ this.isFx31 = this.isFx35; this.isFx36 = appInfo.platformVersion.indexOf('1.9.2') === 0; this.isFx4 = appInfo.platformVersion[0] >= 2; + this.isFx5 = appInfo.platformVersion[0] >= 5; this.isStandalone = appInfo.ID == ZOTERO_CONFIG['GUID']; if(this.isStandalone) { @@ -242,6 +257,9 @@ var Zotero = new function(){ this.isLinux = (this.platform.substr(0, 5) == "Linux"); this.oscpu = win.navigator.oscpu; + // Browser + Zotero.browser = "g"; + // Locale var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefService); @@ -275,19 +293,19 @@ var Zotero = new function(){ xmlhttp.send(null); var matches = xmlhttp.responseText.match(/(ltr|rtl)/); if (matches && matches[0] == 'rtl') { - this.dir = 'rtl'; + Zotero.dir = 'rtl'; } else { - this.dir = 'ltr'; + Zotero.dir = 'ltr'; } try { - var dataDir = this.getZoteroDirectory(); + var dataDir = Zotero.getZoteroDirectory(); } catch (e) { // Zotero dir not found if (e.name == 'NS_ERROR_FILE_NOT_FOUND') { - this.startupError = Zotero.getString('dataDir.notFound'); + Zotero.startupError = Zotero.getString('dataDir.notFound'); _startupErrorHandler = function() { var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); @@ -300,7 +318,7 @@ var Zotero = new function(){ + (ps.BUTTON_POS_2) * (ps.BUTTON_TITLE_IS_STRING); var index = ps.confirmEx(win, Zotero.getString('general.error'), - this.startupError + '\n\n' + + Zotero.startupError + '\n\n' + Zotero.getString('dataDir.previousDir') + ' ' + Zotero.Prefs.get('lastDataDir'), buttonFlags, null, @@ -382,7 +400,6 @@ var Zotero = new function(){ else if (index == 2) { Zotero.chooseZoteroDirectory(true); } - var dataDir = this.getZoteroDirectory(); } // DEBUG: handle more startup errors else { @@ -391,6 +408,54 @@ var Zotero = new function(){ } } + Zotero.IPC.init(); + + // Load additional info for connector or not + if(Zotero.isConnector) { + Zotero.debug("Loading in connector mode"); + Zotero.Connector.init(); + } else { + Zotero.debug("Loading in full mode"); + _initFull(); + } + + this.initialized = true; + + // Register shutdown handler to call Zotero.shutdown() + var _shutdownObserver = {observe:Zotero.shutdown}; + observerService.addObserver(_shutdownObserver, "quit-application", false); + + // Add shutdown listerner to remove observer + this.addShutdownListener(function() { + observerService.removeObserver(_shutdownObserver, "quit-application", false); + }); + + Zotero.debug("Initialized in "+((new Date()).getTime() - start)+" ms"); + + if(!Zotero.isFirstLoadThisSession) { + if(Zotero.isConnector) { + // wait for initComplete message if we switched to connector because standalone was + // started + _waitingForInitComplete = true; + while(_waitingForInitComplete) Zotero.mainThread.processNextEvent(true); + } + + // trigger zotero-reloaded event + Zotero.debug('Triggering "zotero-reloaded" event'); + observerService.notifyObservers(Zotero, "zotero-reloaded", null); + } + + // Broadcast initComplete message if desired + if(_broadcastInitComplete) Zotero.IPC.broadcast("initComplete"); + + return true; + } + + /** + * Initialization function to be called only if Zotero is in full mode + */ + function _initFull() { + var dataDir = Zotero.getZoteroDirectory(); Zotero.VersionHeader.init(); // Check for DB restore @@ -412,7 +477,7 @@ var Zotero = new function(){ Zotero.Schema.skipDefaultData = true; Zotero.Schema.updateSchema(); - this.restoreFromServer = true; + Zotero.restoreFromServer = true; } catch (e) { // Restore from backup? @@ -420,54 +485,7 @@ var Zotero = new function(){ } } - try { - // Test read access - Zotero.DB.test(); - - var dbfile = Zotero.getZoteroDatabase(); - - // Test write access on Zotero data directory - if (!dbfile.parent.isWritable()) { - var msg = 'Cannot write to ' + dbfile.parent.path + '/'; - } - // Test write access on Zotero database - else if (!dbfile.isWritable()) { - var msg = 'Cannot write to ' + dbfile.path; - } - else { - var msg = false; - } - - if (msg) { - var e = { - name: 'NS_ERROR_FILE_ACCESS_DENIED', - message: msg, - toString: function () { - return this.name + ': ' + this.message; - } - }; - throw (e); - } - } - catch (e) { - if (e.name == 'NS_ERROR_FILE_ACCESS_DENIED') { - var msg = Zotero.localeJoin([ - Zotero.getString('startupError.databaseCannotBeOpened'), - Zotero.getString('startupError.checkPermissions') - ]); - this.startupError = msg; - } else if(e.name == "NS_ERROR_STORAGE_BUSY" || e.result == 2153971713) { - var msg = Zotero.localeJoin([ - Zotero.getString('startupError.databaseInUse'), - Zotero.getString(Zotero.isStandalone ? 'startupError.closeFirefox' : 'startupError.closeStandalone') - ]); - this.startupError = msg; - } - - Components.utils.reportError(e); - this.skipLoading = true; - return; - } + if(!_initDB()) return; // Add notifier queue callbacks to the DB layer Zotero.DB.addCallback('begin', Zotero.Notifier.begin); @@ -477,7 +495,7 @@ var Zotero = new function(){ Zotero.Fulltext.init(); // Require >=2.1b3 database to ensure proper locking - if (this.isStandalone && Zotero.Schema.getDBVersion('system') > 0 && Zotero.Schema.getDBVersion('system') < 31) { + if (Zotero.isStandalone && Zotero.Schema.getDBVersion('system') > 0 && Zotero.Schema.getDBVersion('system') < 31) { var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"] .getService(Components.interfaces.nsIAppStartup); @@ -541,7 +559,7 @@ var Zotero = new function(){ appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit); } - this.skipLoading = true; + Zotero.skipLoading = true; return false; } @@ -549,7 +567,7 @@ var Zotero = new function(){ if (Zotero.Schema.userDataUpgradeRequired()) { var upgraded = Zotero.Schema.showUpgradeWizard(); if (!upgraded) { - this.skipLoading = true; + Zotero.skipLoading = true; return false; } } @@ -567,12 +585,12 @@ var Zotero = new function(){ ]) + "\n\n" + Zotero.getString('startupError.zoteroVersionIsOlder.current', Zotero.version) + "\n\n" + Zotero.getString('general.seeForMoreInformation', kbURL); - this.startupError = msg; + Zotero.startupError = msg; } else { - this.startupError = Zotero.getString('startupError.databaseUpgradeError'); + Zotero.startupError = Zotero.getString('startupError.databaseUpgradeError'); } - this.skipLoading = true; + Zotero.skipLoading = true; Components.utils.reportError(e); return false; } @@ -589,8 +607,8 @@ var Zotero = new function(){ // Initialize various services Zotero.Integration.init(); - if(Zotero.Prefs.get("connector.enabled")) { - Zotero.Connector.init(); + if(Zotero.Prefs.get("httpServer.enabled")) { + Zotero.Server.init(); } Zotero.Zeroconf.init(); @@ -607,18 +625,108 @@ var Zotero = new function(){ // Initialize Locate Manager Zotero.LocateManager.init(); - this.initialized = true; - Zotero.debug("Initialized in "+((new Date()).getTime() - start)+" ms"); + return true; + } + + /** + * Initializes the DB connection + */ + function _initDB() { + try { + // Test read access + Zotero.DB.test(); + + var dbfile = Zotero.getZoteroDatabase(); + + // Test write access on Zotero data directory + if (!dbfile.parent.isWritable()) { + var msg = 'Cannot write to ' + dbfile.parent.path + '/'; + } + // Test write access on Zotero database + else if (!dbfile.isWritable()) { + var msg = 'Cannot write to ' + dbfile.path; + } + else { + var msg = false; + } + + if (msg) { + var e = { + name: 'NS_ERROR_FILE_ACCESS_DENIED', + message: msg, + toString: function () { + return Zotero.name + ': ' + Zotero.message; + } + }; + throw (e); + } + } + catch (e) { + if (e.name == 'NS_ERROR_FILE_ACCESS_DENIED') { + var msg = Zotero.localeJoin([ + Zotero.getString('startupError.databaseCannotBeOpened'), + Zotero.getString('startupError.checkPermissions') + ]); + Zotero.startupError = msg; + } else if(e.name == "NS_ERROR_STORAGE_BUSY" || e.result == 2153971713) { + if(Zotero.isStandalone) { + // Standalone should force Fx to release lock + if(Zotero.IPC.broadcast("releaseLock")) { + _waitingForDBLock = true; + while(_waitingForDBLock) Zotero.mainThread.processNextEvent(true); + // we will want to broadcast when initialization completes + _broadcastInitComplete = true; + return _initDB(); + } + } else { + // Fx should start as connector if Standalone is running + var haveStandalone = Zotero.IPC.broadcast("test"); + if(haveStandalone) { + throw "ZOTERO_SHOULD_START_AS_CONNECTOR"; + } + } + + var msg = Zotero.localeJoin([ + Zotero.getString('startupError.databaseInUse'), + Zotero.getString(Zotero.isStandalone ? 'startupError.closeFirefox' : 'startupError.closeStandalone') + ]); + Zotero.startupError = msg; + } + + Components.utils.reportError(e); + Zotero.skipLoading = true; + return false; + } return true; } + /** + * Called when the DB has been released by another Zotero process to perform necessary + * initialization steps + */ + this.onDBLockReleased = function() { + if(Zotero.isConnector) { + // if DB lock is released, switch out of connector mode + switchConnectorMode(false); + } else if(_waitingForDBLock) { + // if waiting for DB lock and we get it, continue init + _waitingForDBLock = false; + } + } + + /** + * Called when an accessory process has been initialized to let use get data + */ + this.onInitComplete = function() { + _waitingForInitComplete = false; + } /* * Check if a DB transaction is open and, if so, disable Zotero */ function stateCheck() { - if (Zotero.DB.transactionInProgress()) { + if(!Zotero.isConnector && Zotero.DB.transactionInProgress()) { this.initialized = false; this.skipLoading = true; return false; @@ -630,7 +738,33 @@ var Zotero = new function(){ this.shutdown = function (subject, topic, data) { Zotero.debug("Shutting down Zotero"); - Zotero.removeTempDirectory(); + + try { + // run shutdown listener + for each(var listener in _shutdownListeners) listener(); + + // remove temp directory + Zotero.removeTempDirectory(); + + if(Zotero.initialized && Zotero.DB) { + Zotero.debug("Closing database"); + + // run GC to finalize open statements + // TODO remove this and finalize statements created with + // Zotero.DBConnection.getStatement() explicitly + Components.utils.forceGC(); + + // unlock DB + Zotero.DB.closeDatabase(); + + // broadcast that DB lock has been released + Zotero.IPC.broadcast("lockReleased"); + } + } catch(e) { + Zotero.debug(e); + throw e; + } + return true; } @@ -1511,6 +1645,12 @@ var Zotero = new function(){ return true; } + /** + * Adds a listener to be called when Zotero shuts down (even if Firefox is not shut down) + */ + this.addShutdownListener = function(listener) { + _shutdownListeners.push(listener); + } function _showWindowZoteroPaneOverlay(doc) { doc.getElementById('zotero-collections-tree').disabled = true; @@ -1658,9 +1798,21 @@ var Zotero = new function(){ Zotero.Creators.reloadAll(); Zotero.Items.reloadAll(); } -}; - - + + /** + * Brings Zotero Standalone to the foreground + */ + this.activateStandalone = function() { + var io = Components.classes['@mozilla.org/network/io-service;1'] + .getService(Components.interfaces.nsIIOService); + var uri = io.newURI('zotero://select', null, null); + var handler = Components.classes['@mozilla.org/uriloader/external-protocol-service;1'] + .getService(Components.interfaces.nsIExternalProtocolService) + .getProtocolHandlerInfo('zotero'); + handler.preferredAction = Components.interfaces.nsIHandlerInfo.useSystemDefault; + handler.launchWithURI(uri, null); + } +}).call(Zotero); Zotero.Prefs = new function(){ // Privileged methods diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 8e8093ef1..dea87d7f7 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -90,18 +90,16 @@ var ZoteroPane = new function() var self = this; var _loaded = false; - var titlebarcolorState, titleState; + var titlebarcolorState, titleState, observerService; + var _reloadFunctions = []; // Also needs to be changed in collectionTreeView.js var _lastViewedFolderRE = /^(?:(C|S|G)([0-9]+)|L)$/; - /* + /** * Called when the window containing Zotero pane is open */ - function init() - { - if(!Zotero || !Zotero.initialized) return; - + function init() { // Set "Report Errors..." label via property rather than DTD entity, // since we need to reference it in script elsewhere document.getElementById('zotero-tb-actions-reportErrors').setAttribute('label', @@ -116,9 +114,9 @@ var ZoteroPane = new function() var zp = document.getElementById('zotero-pane'); Zotero.setFontSize(zp); - this.updateToolbarPosition(); - window.addEventListener("resize", this.updateToolbarPosition, false); - window.setTimeout(this.updateToolbarPosition, 0); + ZoteroPane_Local.updateToolbarPosition(); + window.addEventListener("resize", ZoteroPane_Local.updateToolbarPosition, false); + window.setTimeout(ZoteroPane_Local.updateToolbarPosition, 0); Zotero.updateQuickSearchBox(document); @@ -136,10 +134,34 @@ var ZoteroPane = new function() zp.setAttribute("ignoreActiveAttribute", "true"); } + // register an observer for Zotero reload + observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver(_reload, "zotero-reloaded", false); + this.addReloadListener(_loadPane); + + // continue loading pane + _loadPane(); + } + + /** + * Called on window load or when has been reloaded after switching into or out of connector + * mode + */ + function _loadPane() { + if(!Zotero || !Zotero.initialized) return; + + if(Zotero.isConnector) { + ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('connector.standaloneOpen')); + return; + } else { + ZoteroPane_Local.clearItemsPaneMessage(); + } + //Initialize collections view - this.collectionsView = new Zotero.CollectionTreeView(); + ZoteroPane_Local.collectionsView = new Zotero.CollectionTreeView(); var collectionsTree = document.getElementById('zotero-collections-tree'); - collectionsTree.view = this.collectionsView; + collectionsTree.view = ZoteroPane_Local.collectionsView; collectionsTree.controllers.appendController(new Zotero.CollectionTreeCommandController(collectionsTree)); collectionsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); @@ -147,8 +169,6 @@ var ZoteroPane = new function() itemsTree.controllers.appendController(new Zotero.ItemTreeCommandController(itemsTree)); itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); - this.buildItemTypeSubMenu(); - var menu = document.getElementById("contentAreaContextMenu"); menu.addEventListener("popupshowing", ZoteroPane_Local.contextPopupShowing, false); @@ -322,6 +342,8 @@ var ZoteroPane = new function() this.collectionsView.unregister(); if (this.itemsView) this.itemsView.unregister(); + + observerService.removeObserver(_reload, "zotero-reloaded", false); } /** @@ -349,6 +371,7 @@ var ZoteroPane = new function() return false; } + this.buildItemTypeSubMenu(); this.unserializePersist(); this.updateToolbarPosition(); this.updateTagSelectorSize(); @@ -3644,6 +3667,22 @@ var ZoteroPane = new function() this.openAboutDialog = function() { window.openDialog('chrome://zotero/content/about.xul', 'about', 'chrome'); } + + /** + * Adds or removes a function to be called when Zotero is reloaded by switching into or out of + * the connector + */ + this.addReloadListener = function(/** @param {Function} **/func) { + if(_reloadFunctions.indexOf(func) === -1) _reloadFunctions.push(func); + } + + /** + * Called when Zotero is reloaded (i.e., if it is switched into or out of connector mode) + */ + function _reload() { + Zotero.debug("Reloading Zotero pane"); + for each(var func in _reloadFunctions) func(); + } } /** diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 046f19857..578543034 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -730,4 +730,6 @@ locate.libraryLookup.label = Library Lookup locate.libraryLookup.tooltip = Look up this item using the selected OpenURL resolver locate.manageLocateEngines = Manage Lookup Engines... -standalone.corruptInstallation = Your Zotero Standalone installation appears to be corrupted due to a failed auto-update. While Zotero may continue to function, to avoid potential bugs, please download the latest version of Zotero Standalone from http://zotero.org/support/standalone as soon as possible. \ No newline at end of file +standalone.corruptInstallation = Your Zotero Standalone installation appears to be corrupted due to a failed auto-update. While Zotero may continue to function, to avoid potential bugs, please download the latest version of Zotero Standalone from http://zotero.org/support/standalone as soon as possible. + +connector.standaloneOpen = Your database cannot be accessed because Zotero Standalone is currently open. Please view your items in Zotero Standalone. \ No newline at end of file diff --git a/components/zotero-integration-service.js b/components/zotero-command-line-handler.js similarity index 58% rename from components/zotero-integration-service.js rename to components/zotero-command-line-handler.js index 14f28e528..5939210f3 100644 --- a/components/zotero-integration-service.js +++ b/components/zotero-command-line-handler.js @@ -32,51 +32,64 @@ https://developer.mozilla.org/en/Chrome/Command_Line */ -const nsISupports = Components.interfaces.nsISupports; -const nsICategoryManager = Components.interfaces.nsICategoryManager; -const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar; -const nsICommandLine = Components.interfaces.nsICommandLine; -const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler; -const nsIFactory = Components.interfaces.nsIFactory; -const nsIModule = Components.interfaces.nsIModule; -const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher; - -const clh_contractID = "@mozilla.org/commandlinehandler/general-startup;1?type=zotero-integration"; +const clh_contractID = "@mozilla.org/commandlinehandler/general-startup;1?type=zotero"; const clh_CID = Components.ID("{531828f8-a16c-46be-b9aa-14845c3b010f}"); -const clh_category = "m-zotero-integration"; -const clh_description = "Zotero Integration Command Line Handler"; +const clh_category = "m-zotero"; +const clh_description = "Zotero Command Line Handler"; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); /** * The XPCOM component that implements nsICommandLineHandler. */ -function ZoteroIntegrationCommandLineHandler() {} -ZoteroIntegrationCommandLineHandler.prototype = { - Zotero : null, - +function ZoteroCommandLineHandler() {} +ZoteroCommandLineHandler.prototype = { /* nsISupports */ - QueryInterface : function(iid) { - if(iid.equals(nsICommandLineHandler) || - iid.equals(nsIFactory) || - iid.equals(nsISupports)) return this; - throw Components.results.NS_ERROR_NO_INTERFACE; - }, + QueryInterface : XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler, + Components.interfaces.nsIFactory, Components.interfaces.nsISupports]), /* nsICommandLineHandler */ handle : function(cmdLine) { + // handler for Zotero integration commands + // this is typically used on Windows only, via WM_COPYDATA rather than the command line var agent = cmdLine.handleFlagWithParam("ZoteroIntegrationAgent", false); - var command = cmdLine.handleFlagWithParam("ZoteroIntegrationCommand", false); - var docId = cmdLine.handleFlagWithParam("ZoteroIntegrationDocument", false); - if(agent && command) { - if(!this.Zotero) this.Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports).wrappedJSObject; - var Zotero = this.Zotero; + if(agent) { + // Don't open a new window + cmdLine.preventDefault = true; + + var command = cmdLine.handleFlagWithParam("ZoteroIntegrationCommand", false); + var docId = cmdLine.handleFlagWithParam("ZoteroIntegrationDocument", false); + // Not quite sure why this is necessary to get the appropriate scoping + var Zotero = this.Zotero; var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); timer.initWithCallback({notify:function() { Zotero.Integration.execCommand(agent, command, docId) }}, 0, Components.interfaces.nsITimer.TYPE_ONE_SHOT); } + + // handler for Windows IPC commands + var param = cmdLine.handleFlagWithParam("ZoteroIPC", false); + if(param) { + // Don't open a new window + cmdLine.preventDefault = true; + this.Zotero.IPC.parsePipeInput(param); + } + + // special handler for "zotero" URIs at the command line to prevent them from opening a new + // window + if(this.Zotero.isStandalone) { + var param = cmdLine.handleFlagWithParam("url", false); + if(param) { + var uri = cmdLine.resolveURI(param); + if(uri.schemeIs("zotero")) { + // Don't open a new window + cmdLine.preventDefault = true; + + Components.classes["@mozilla.org/network/protocol;1?name=zotero"] + .createInstance(Components.interfaces.nsIProtocolHandler).newChannel(uri); + } + } + } }, classDescription: clh_description, @@ -88,12 +101,20 @@ ZoteroIntegrationCommandLineHandler.prototype = { Components.interfaces.nsISupports]) }; +ZoteroCommandLineHandler.prototype.__defineGetter__("Zotero", function() { + if(!this._Zotero) { + this._Zotero = Components.classes["@zotero.org/Zotero;1"] + .getService(Components.interfaces.nsISupports).wrappedJSObject; + } + return this._Zotero; +}); + /** * XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). * XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). */ if (XPCOMUtils.generateNSGetFactory) { - var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroIntegrationCommandLineHandler]); + var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroCommandLineHandler]); } else { - var NSGetModule = XPCOMUtils.generateNSGetModule([ZoteroIntegrationCommandLineHandler]); + var NSGetModule = XPCOMUtils.generateNSGetModule([ZoteroCommandLineHandler]); } \ No newline at end of file diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js index f1f7d5efb..bbd59eb4d 100644 --- a/components/zotero-protocol-handler.js +++ b/components/zotero-protocol-handler.js @@ -852,13 +852,21 @@ function ChromeExtensionHandler() { var [path, queryString] = uri.path.substr(1).split('?'); var [type, id] = path.split('/'); - //currently only able to select one item + // currently only able to select one item var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] .getService(Components.interfaces.nsIWindowMediator); - var win = wm.getMostRecentWindow(null); + var win = wm.getMostRecentWindow("navigator:browser"); + // restore window if it's in the dock + if(win.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { + win.restore(); + } + + // open Zotero pane win.ZoteroPane.show(); + if(!id) return; + var lkh = Zotero.Items.parseLibraryKeyHash(id); if (lkh) { var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); @@ -1026,10 +1034,10 @@ function ChromeExtensionHandler() { try { var originalURI = uri.path; originalURI = decodeURIComponent(originalURI.substr(originalURI.indexOf("/")+1)); - if(!Zotero.Connector.Data[originalURI]) { + if(!Zotero.Server.Connector.Data[originalURI]) { return null; } else { - return new ConnectorChannel(originalURI, Zotero.Connector.Data[originalURI]); + return new ConnectorChannel(originalURI, Zotero.Server.Connector.Data[originalURI]); } } catch(e) { Zotero.debug(e); diff --git a/components/zotero-service.js b/components/zotero-service.js index 28b11be55..b8a5aa442 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -35,30 +35,31 @@ const ZOTERO_IID = Components.interfaces.chnmIZoteroService; //unused const Cc = Components.classes; const Ci = Components.interfaces; -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]. - getService(Components.interfaces.nsIXULAppInfo); -if(appInfo.platformVersion[0] >= 2) { - Components.utils.import("resource://gre/modules/AddonManager.jsm"); -} - -// Assign the global scope to a variable to passed via wrappedJSObject -var ZoteroWrapped = this; - -/******************************************************************** -* Include the core objects to be stored within XPCOM -*********************************************************************/ - -var xpcomFiles = [ +/** XPCOM files to be loaded for all modes **/ +const xpcomFilesAll = [ 'zotero', + 'date', + 'debug', + 'error', + 'file', + 'http', + 'mimeTypeHandler', + 'openurl', + 'ipc', + 'progressWindow', + 'translation/translate', + 'translation/translate_firefox', + 'translation/tlds', + 'utilities' +]; + +/** XPCOM files to be loaded only for local translation and DB access **/ +const xpcomFilesLocal = [ + 'collectionTreeView', 'annotate', 'attachments', 'cite', - 'collectionTreeView', 'commons', - 'connector', - 'dataServer', 'data_access', 'data/dataObjects', 'data/cachedTypes', @@ -79,28 +80,21 @@ var xpcomFiles = [ 'data/tags', 'date', 'db', - 'debug', 'duplicate', 'enstyle', - 'error', - 'file', 'fulltext', - 'http', 'id', 'integration', - 'integration_compat', 'itemTreeView', 'locateManager', 'mime', - 'mimeTypeHandler', 'notifier', - 'openurl', - 'progressWindow', 'proxy', 'quickCopy', 'report', 'schema', 'search', + 'server', 'style', 'sync', 'storage', @@ -108,117 +102,197 @@ var xpcomFiles = [ 'storage/zfs', 'storage/webdav', 'timeline', - 'translation/translator', - 'translation/translate', - 'translation/browser_firefox', - 'translation/item_local', 'uri', - 'utilities', - 'zeroconf' + 'zeroconf', + 'translation/translate_item', + 'translation/translator', + 'server_connector' ]; -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFiles[0] + ".js"); - -// Load CiteProc into Zotero.CiteProc namespace -Zotero.CiteProc = {"Zotero":Zotero}; -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/citeproc.js", Zotero.CiteProc); - -for (var i=1; i