www

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

commit 8268d1b01c7363107cba5cc7eb72d0c3c9beae46
parent fd5d9496fde2386916e7aa65e366307e9841da80
Author: Simon Kornblith <simon@simonster.com>
Date:   Tue, 14 Jun 2011 00:36:21 +0000

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


Diffstat:
Mchrome.manifest | 6+++---
Mchrome/content/zotero/browser.js | 38++++++++++++++++++++++----------------
Mchrome/content/zotero/overlay.js | 41++++++++++++++++++++++++++++++-----------
Mchrome/content/zotero/recognizePDF.js | 8+++++---
Dchrome/content/zotero/xpcom/connector.js | 781-------------------------------------------------------------------------------
Achrome/content/zotero/xpcom/connector/cachedTypes.js | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/connector/connector.js | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/connector/translate_item.js | 41+++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/connector/translator.js | 342+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/date.js | 2+-
Mchrome/content/zotero/xpcom/db.js | 31++++++++++++++++++-------------
Mchrome/content/zotero/xpcom/debug.js | 11++++++++---
Mchrome/content/zotero/xpcom/integration.js | 333+++++++++----------------------------------------------------------------------
Dchrome/content/zotero/xpcom/integration_worker.js | 73-------------------------------------------------------------------------
Achrome/content/zotero/xpcom/ipc.js | 485+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/mimeTypeHandler.js | 3+++
Achrome/content/zotero/xpcom/pipe_worker.js | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/proxy.js | 6++----
Achrome/content/zotero/xpcom/server.js | 380+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/server_connector.js | 608++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dchrome/content/zotero/xpcom/translation/browser_firefox.js | 503-------------------------------------------------------------------------------
Dchrome/content/zotero/xpcom/translation/browser_other.js | 67-------------------------------------------------------------------
Dchrome/content/zotero/xpcom/translation/item_connector.js | 31-------------------------------
Achrome/content/zotero/xpcom/translation/tlds.js | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/translation/translate.js | 462++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Achrome/content/zotero/xpcom/translation/translate_firefox.js | 503+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rchrome/content/zotero/xpcom/translation/item_local.js -> chrome/content/zotero/xpcom/translation/translate_item.js | 0
Mchrome/content/zotero/xpcom/translation/translator.js | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mchrome/content/zotero/xpcom/utilities.js | 49++++++++++++++++++++++++++++++++++---------------
Mchrome/content/zotero/xpcom/zotero.js | 310+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mchrome/content/zotero/zoteroPane.js | 65++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mchrome/locale/en-US/zotero/zotero.properties | 6++++--
Acomponents/zotero-command-line-handler.js | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcomponents/zotero-integration-service.js | 100-------------------------------------------------------------------------------
Mcomponents/zotero-protocol-handler.js | 16++++++++++++----
Mcomponents/zotero-service.js | 318+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mdefaults/preferences/zotero.js | 4++--
37 files changed, 4246 insertions(+), 2304 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 <http://www.gnu.org/licenses/>. - - ***** 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"]] = "<html>"+this._parsedPostData["html"]+"</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 @@ -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 <http://www.gnu.org/licenses/>. + + ***** 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<schemaTypes.length; i++) { + var schemaType = schemaTypes[i]; + this[schemaType] = typeSchema[schemaType]; + for(var id in this[schemaType]) { + var entry = this[schemaType][id]; + entry.id = id; + this[schemaType][entry.name] = entry; + } + } + } +} + +Zotero.CachedTypes = function() { + this.getID = function(idOrName) { + if(!Zotero.Connector.Types[this.schemaType][idOrName]) return false; + return Zotero.Connector.Types[this.schemaType][idOrName].id; + } + + this.getName = function(idOrName) { + if(!Zotero.Connector.Types[this.schemaType][idOrName]) return false; + return Zotero.Connector.Types[this.schemaType][idOrName].name; + } + + this.getLocalizedString = function(idOrName) { + if(!Zotero.Connector.Types[this.schemaType][idOrName]) return false; + return Zotero.Connector.Types[this.schemaType][idOrName].localizedString; + } +} + +Zotero.ItemTypes = new function() { + this.schemaType = "itemTypes"; + Zotero.CachedTypes.call(this); + + this.getImageSrc = function(idOrName) { + if(!Zotero.Connector.Types["itemTypes"][idOrName]) return false; + + if(Zotero.isFx) { + return "chrome://zotero/skin/"+Zotero.Connector.Types["itemTypes"][idOrName].icon; + } else { + return "images/"+Zotero.Connector.Types["itemTypes"][idOrName].icon; + } + } +} + +Zotero.CreatorTypes = new function() { + this.schemaType = "creatorTypes"; + Zotero.CachedTypes.call(this); + + this.getTypesForItemType = function(idOrName) { + if(!Zotero.Connector.Types["itemTypes"][idOrName]) return false; + var itemType = Zotero.Connector.Types["itemTypes"][idOrName]; + var creatorTypes = []; + for(var i=0; i<itemType.creatorTypes.length; i++) { + creatorTypes.push(Zotero.Connector.Types["creatorTypes"][itemType.creatorTypes[i]]); + } + return creatorTypes; + } +} + +Zotero.ItemFields = new function() { + this.schemaType = "fields"; + Zotero.CachedTypes.call(this); + + this.isValidForType = function(fieldIdOrName, typeIdOrName) { + // mimics itemFields.js + if(!Zotero.Connector.Types["fields"][fieldIdOrName] + || !Zotero.Connector.Types["itemTypes"][typeIdOrName]) throw "Invalid field or type ID"; + + return Zotero.Connector.Types["itemTypes"][typeIdOrName].fields.indexOf( + Zotero.Connector.Types["fields"][fieldIdOrName].id) !== -1; + } +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/connector/connector.js b/chrome/content/zotero/xpcom/connector/connector.js @@ -0,0 +1,176 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2011 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ +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/connector/translate_item.js b/chrome/content/zotero/xpcom/connector/translate_item.js @@ -0,0 +1,40 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +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 diff --git a/chrome/content/zotero/xpcom/connector/translator.js 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 <http://www.gnu.org/licenses/>. + + ***** 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<translators.length; i++) { + var translator = new Zotero.Translator(translators[i]); + _translators[translator.translatorID] = translator; + + for(var type in TRANSLATOR_TYPES) { + if(translator.translatorType & TRANSLATOR_TYPES[type]) { + _cache[type].push(translator); + } + } + } + + // Sort by priority + var cmp = function (a, b) { + if (a.priority > 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; i<hostnames.length-2; i++) { + if(TLDS[hostnames[i].toLowerCase()]) { + var properHost = hostnames.slice(0, i+1).join("."); + searchURIs.push(m[1]+properHost+uri.substr(m[0].length)); + properHosts.push(properHost); + proxyHosts.push(hostnames.slice(i+1).join(".")); + } + } + } + + var converterFunctions = []; + for(var i=0; i<allTranslators.length; i++) { + for(var j=0; j<searchURIs.length; j++) { + // don't attempt to use translators with no target that can't be run in this browser + // since that would require transmitting every page to Zotero host + if(!allTranslators[i].webRegexp + && allTranslators[i].runMode !== Zotero.Translator.RUN_MODE_IN_BROWSER) { + continue; + } + + if(!allTranslators[i].webRegexp + || (uri.length < 8192 && allTranslators[i].webRegexp.test(searchURIs[j]))) { + // add translator to list + potentialTranslators.push(allTranslators[i]); + + if(j === 0) { + converterFunctions.push(null); + } else if(Zotero.isFx) { + // in Firefox, push the converterFunction + converterFunctions.push(new function() { + var re = new RegExp('^https?://(?:[^/]\\.)?'+Zotero.Utilities.quotemeta(properHosts[j-1]), "gi"); + var proxyHost = proxyHosts[j-1].replace(/\$/g, "$$$$"); + return function(uri) { return uri.replace(re, "$&."+proxyHost) }; + }); + } else { + // in Chrome/Safari, the converterFunction needs to be passed as JSON, so + // just push an array with the proper and proxyHosts + converterFunctions.push([properHosts[j-1], proxyHosts[j-1]]); + } + + // don't add translator more than once + break; + } + } + } + + new Zotero.Translators.CodeGetter(potentialTranslators, callback, + [potentialTranslators, converterFunctions]); + return true; + } + + /** + * Converts translators to JSON-serializable objects + */ + this.serialize = function(translator) { + // handle translator arrays + if(translator.length !== undefined) { + var newTranslators = new Array(translator.length); + for(var i in translator) { + newTranslators[i] = Zotero.Translators.serialize(translator[i]); + } + return newTranslators; + } + + // handle individual translator + var newTranslator = {}; + for(var i in PRESERVE_PROPERTIES) { + var property = PRESERVE_PROPERTIES[i]; + newTranslator[property] = translator[property]; + } + return newTranslator; + } +} + +/** + * A class to get the code for a set of translators at once + */ +Zotero.Translators.CodeGetter = function(translators, callback, callbackArgs) { + this._translators = translators; + this._callbackArgs = callbackArgs; + this._callback = callback; + this.getCodeFor(0); +} + +Zotero.Translators.CodeGetter.prototype.getCodeFor = function(i) { + var me = this; + while(true) { + if(i === this._translators.length) { + // all done; run callback + this._callback(this._callbackArgs); + return; + } + + if(this._translators[i].runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { + // get next translator + this._translators[i].getCode(function() { me.getCodeFor(i+1) }); + return; + } + + // if we are not at end of list and there is no reason to retrieve the code, keep going + // through the list of potential translators + i++; + } +} + +const TRANSLATOR_PROPERTIES = ["translatorID", "translatorType", "label", "creator", "target", + "priority", "browserSupport"]; +var PRESERVE_PROPERTIES = TRANSLATOR_PROPERTIES.concat(["displayOptions", "configOptions", + "code", "runMode"]); +/** + * @class Represents an individual translator + * @constructor + * @property {String} translatorID Unique GUID of the translator + * @property {Integer} translatorType Type of the translator (use bitwise & with TRANSLATOR_TYPES to read) + * @property {String} label Human-readable name of the translator + * @property {String} creator Author(s) of the translator + * @property {String} target Location that the translator processes + * @property {String} minVersion Minimum Zotero version + * @property {String} maxVersion Minimum Zotero version + * @property {Integer} priority Lower-priority translators will be selected first + * @property {String} browserSupport String indicating browser supported by the translator + * g = Gecko (Firefox) + * c = Google Chrome (WebKit & V8) + * s = Safari (WebKit & Nitro/Squirrelfish Extreme) + * i = Internet Explorer + * @property {Object} configOptions Configuration options for import/export + * @property {Object} displayOptions Display options for export + * @property {Boolean} inRepository Whether the translator may be found in the repository + * @property {String} lastUpdated SQL-style date and time of translator's last update + * @property {String} code The executable JavaScript for the translator + */ +Zotero.Translator = function(info) { + // make sure we have all the properties + for(var i in TRANSLATOR_PROPERTIES) { + var property = TRANSLATOR_PROPERTIES[i]; + if(info[property] === undefined) { + this.logError('Missing property "'+property+'" in translator metadata JSON object in ' + info.label); + haveMetadata = false; + break; + } else { + this[property] = info[property]; + } + } + + if(info["browserSupport"].indexOf(Zotero.browser) !== -1) { + this.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER; + } else { + this.runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE; + } + + this._configOptions = info["configOptions"] ? info["configOptions"] : {}; + this._displayOptions = info["displayOptions"] ? info["displayOptions"] : {}; + + if(this.translatorType & TRANSLATOR_TYPES["import"]) { + // compile import regexp to match only file extension + this.importRegexp = this.target ? new RegExp("\\."+this.target+"$", "i") : null; + } + + if(this.translatorType & TRANSLATOR_TYPES["web"]) { + // compile web regexp + this.webRegexp = this.target ? new RegExp(this.target, "i") : null; + } + + if(info.code) { + this.code = info.code; + } +} + +Zotero.Translator.prototype.getCode = function(callback) { + if(this.code) { + callback(true); + } else { + var me = this; + Zotero.Connector.callMethod("getTranslatorCode", {"translatorID":this.translatorID}, + function(code) { + if(!code) { + callback(false); + } else { + me.code = code; + callback(true); + } + } + ); + } +} + +Zotero.Translator.prototype.__defineGetter__("displayOptions", function() { + return Zotero.Utilities.deepCopy(this._displayOptions); +}); +Zotero.Translator.prototype.__defineGetter__("configOptions", function() { + return Zotero.Utilities.deepCopy(this._configOptions); +}); + +/** + * Log a translator-related error + * @param {String} message The error message + * @param {String} [type] The error type ("error", "warning", "exception", or "strict") + * @param {String} [line] The text of the line on which the error occurred + * @param {Integer} lineNumber + * @param {Integer} colNumber + */ +Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, colNumber) { + Zotero.log(message, type ? type : "error", this.label); +} + +Zotero.Translator.RUN_MODE_IN_BROWSER = 1; +Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE = 2; +Zotero.Translator.RUN_MODE_ZOTERO_SERVER = 4; +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/date.js b/chrome/content/zotero/xpcom/date.js @@ -61,7 +61,7 @@ Zotero.Date = new function(){ .getService(Components.interfaces.nsIStringBundleService).createBundle(src, appLocale); _months = {"short":[], "long":[]}; - for(let i=1; i<=12; i++) { + for(var i=1; i<=12; i++) { _months.short.push(bundle.GetStringFromName("month."+i+".Mmm")); _months.long.push(bundle.GetStringFromName("month."+i+".name")); } diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -129,7 +129,7 @@ Zotero.DBConnection.prototype.query = function (sql,params) { } dataset.push(row); } - statement.reset(); + statement.finalize(); return dataset.length ? dataset : false; } @@ -170,12 +170,12 @@ Zotero.DBConnection.prototype.valueQuery = function (sql,params) { // No rows if (!statement.executeStep()) { - statement.reset(); + statement.finalize(); return false; } var value = this._getTypedValue(statement, 0); - statement.reset(); + statement.finalize(); return value; } @@ -202,7 +202,7 @@ Zotero.DBConnection.prototype.columnQuery = function (sql,params) { while (statement.executeStep()) { column.push(this._getTypedValue(statement, 0)); } - statement.reset(); + statement.finalize(); return column.length ? column : false; } return false; @@ -630,7 +630,7 @@ Zotero.DBConnection.prototype.getColumns = function (table) { for (var i=0,len=statement.columnCount; i<len; i++) { cols.push(statement.getColumnName(i)); } - statement.reset(); + statement.finalize(); return cols; } catch (e) { @@ -771,8 +771,11 @@ Zotero.DBConnection.prototype.checkException = function (e) { Zotero.DBConnection.prototype.closeDatabase = function () { - var db = this._getDBConnection(); - db.close(); + if(this._connection) { + this.stopDummyStatement(); + this._connection.close(); + return true; + } } @@ -855,10 +858,10 @@ Zotero.DBConnection.prototype.backupDatabase = function (suffix) { var hadDummyStatement = !!this._dummyStatement; try { if (dbLockExclusive) { - Zotero.DB.query("PRAGMA locking_mode=NORMAL"); + this.query("PRAGMA locking_mode=NORMAL"); } if (hadDummyStatement) { - Zotero.DB.stopDummyStatement(); + this.stopDummyStatement(); } var store = Components.classes["@mozilla.org/storage/service;1"]. @@ -872,10 +875,10 @@ Zotero.DBConnection.prototype.backupDatabase = function (suffix) { } finally { if (dbLockExclusive) { - Zotero.DB.query("PRAGMA locking_mode=EXCLUSIVE"); + this.query("PRAGMA locking_mode=EXCLUSIVE"); } if (hadDummyStatement) { - Zotero.DB.startDummyStatement(); + this.startDummyStatement(); } } @@ -1003,8 +1006,10 @@ Zotero.DBConnection.prototype.stopDummyStatement = function () { } Zotero.debug("Stopping dummy statement for '" + this._dbName + "'"); - this._dummyStatement.reset(); - this._dummyStatement = null; + this._dummyStatement.finalize(); + this._dummyConnection.close(); + delete this._dummyConnection; + delete this._dummyStatement; } diff --git a/chrome/content/zotero/xpcom/debug.js b/chrome/content/zotero/xpcom/debug.js @@ -25,8 +25,8 @@ Zotero.Debug = new function () { - this.__defineGetter__('storing', function () _store); - this.__defineGetter__('enabled', function () _console || _store); + this.__defineGetter__('storing', function () { return _store; }); + this.__defineGetter__('enabled', function () { return _console || _store; }); var _console; var _store; @@ -81,7 +81,12 @@ Zotero.Debug = new function () { } if (_console) { - dump('zotero(' + level + ')' + (_time ? deltaStr : '') + ': ' + message + "\n\n"); + var output = 'zotero(' + level + ')' + (_time ? deltaStr : '') + ': ' + message; + if(Zotero.isFx) { + dump(output+"\n\n"); + } else { + console.log(output); + } } if (_store) { if (Math.random() < 1/1000) { diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -31,7 +31,6 @@ const DATA_VERSION = 3; // this is used only for update checking const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; -const INTEGRATION_MIN_VERSIONS = ["3.1.2", "3.1b1", "3.1b1"]; Zotero.Integration = new function() { var _fifoFile = null; @@ -39,8 +38,7 @@ Zotero.Integration = new function() { var _osascriptFile; var _inProgress = false; var _integrationVersionsOK = null; - var _pipeMode = false; - var _winUser32; + var INTEGRATION_MIN_VERSIONS; // these need to be global because of GC var _timer; @@ -52,29 +50,32 @@ Zotero.Integration = new function() { * Initializes the pipe used for integration on non-Windows platforms. */ this.init = function() { - // initialize SOAP server just to throw version errors - Zotero.Integration.Compat.init(); + if(Zotero.isMac || Zotero.isWin) { // on Mac or Windows, we don't have pipe issues + INTEGRATION_MIN_VERSIONS = ["3.1.2", "3.1b1", "3.1b1"]; + } else { // on *NIX, there's no point in supporting 3.1b1 + INTEGRATION_MIN_VERSIONS = ["3.1.2", "3.5a1", "3.1b1"]; + } - // Windows uses a command line handler for integration. See + // We only use an integration pipe on OS X. + // On Linux, we use the alternative communication method in the OOo plug-in + // On Windows, we use a command line handler for integration. See // components/zotero-integration-service.js for this implementation. - if(Zotero.isWin) return; + if(!Zotero.isMac) return; // Determine where to put the pipe - if(Zotero.isMac) { - // on OS X, first try /Users/Shared for those who can't put pipes in their home - // directories - _fifoFile = Components.classes["@mozilla.org/file/local;1"]. - createInstance(Components.interfaces.nsILocalFile); - _fifoFile.initWithPath("/Users/Shared"); - - if(_fifoFile.exists() && _fifoFile.isDirectory() && _fifoFile.isWritable()) { - var logname = Components.classes["@mozilla.org/process/environment;1"]. - getService(Components.interfaces.nsIEnvironment). - get("LOGNAME"); - _fifoFile.append(".zoteroIntegrationPipe_"+logname); - } else { - _fifoFile = null; - } + // on OS X, first try /Users/Shared for those who can't put pipes in their home + // directories + _fifoFile = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + _fifoFile.initWithPath("/Users/Shared"); + + if(_fifoFile.exists() && _fifoFile.isDirectory() && _fifoFile.isWritable()) { + var logname = Components.classes["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.nsIEnvironment). + get("LOGNAME"); + _fifoFile.append(".zoteroIntegrationPipe_"+logname); + } else { + _fifoFile = null; } if(!_fifoFile) { @@ -85,8 +86,6 @@ Zotero.Integration = new function() { _fifoFile.append(".zoteroIntegrationPipe"); } - Zotero.debug("Initializing Zotero integration pipe at "+_fifoFile.path); - // destroy old pipe, if one exists try { if(_fifoFile.exists()) { @@ -100,35 +99,27 @@ Zotero.Integration = new function() { + "See http://forums.zotero.org/discussion/12054/#Item_10 " + "for instructions on correcting this problem." ); - if(Zotero.isMac) { - // can attempt to delete on OS X - try { - var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe")); - if(!deletePipe) return; - let escapedFifoFile = _fifoFile.path.replace("'", "'\\''"); - _executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); - if(_fifoFile.exists()) return; - } catch(e) { - Zotero.logError(e); - return; - } + + // can attempt to delete on OS X + try { + var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe")); + if(!deletePipe) return; + let escapedFifoFile = _fifoFile.path.replace("'", "'\\''"); + _executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); + if(_fifoFile.exists()) return; + } catch(e) { + Zotero.logError(e); + return; } } // try to initialize pipe try { - var pipeInitialized = _initializeIntegrationPipe(); + Zotero.IPC.Pipe.initPipeListener(_fifoFile, _parseIntegrationPipeCommand); } catch(e) { - Components.utils.reportError(e); - } - - if(pipeInitialized) { - // if initialization succeeded, add an observer so that we don't hang shutdown - var observerService = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - observerService.addObserver({ observe: Zotero.Integration.destroy }, "quit-application", false); + Zotero.logError(e); } _updateTimer = Components.classes["@mozilla.org/timer;1"]. @@ -210,244 +201,12 @@ Zotero.Integration = new function() { if(parts) { var agent = parts[1].toString(); var cmd = parts[2].toString(); - - // return if we were told to shutdown - if(agent === "Zotero" && cmd === "shutdown") return; - - _initializePipeStreamPump(); - var document = parts[3] ? parts[3].toString() : null; Zotero.Integration.execCommand(agent, cmd, document); } else { - _initializePipeStreamPump(); Components.utils.reportError("Zotero: Invalid integration input received: "+string); } - } else { - _initializePipeStreamPump(); - } - } - - /** - * Listens asynchronously for data on the integration pipe and reads it when available - * - * Used to read from the integration pipe on Fx 4.2 - */ - var _integrationPipeListenerFx42 = { - "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(); - - _parseIntegrationPipeCommand(out.value); - }}; - - /** - * Polling mechanism for file - */ - var _integrationPipeObserverFx36 = {"notify":function() { - if(_fifoFile.fileSize === 0) return; - - // read from pipe (file, actually) - var string = Zotero.File.getContents(_fifoFile); - - // 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(); - - // run command - _parseIntegrationPipeCommand(string); - }}; - - /** - * Initializes the nsIInputStream and nsIInputStreamPump to read from _fifoFile - */ - function _initializePipeStreamPump() { - // Fx >4 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; } /** @@ -547,22 +306,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 */ this.activate = function() { diff --git a/chrome/content/zotero/xpcom/integration_worker.js b/chrome/content/zotero/xpcom/integration_worker.js @@ -1,72 +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 <http://www.gnu.org/licenses/>. - - ***** END LICENSE BLOCK ***** -*/ - -onmessage = function(event) { - var path = event.data.path; - - // ctypes declarations follow - var lib = ctypes.open(event.data.libc); - - // int open(const char *path, int oflag, ...); - var open = lib.declare("open", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int); - - // ssize_t read(int fildes, void *buf, size_t nbyte); - var read = lib.declare("read", ctypes.default_abi, ctypes.ssize_t, ctypes.int, - ctypes.char.ptr, ctypes.size_t); - - // int close(int fildes); - var close = lib.declare("close", ctypes.default_abi, ctypes.int, ctypes.int); - - // define buffer for reading from fifo - const BUFFER_SIZE = 4096; - - while(true) { - var buf = ctypes.char.array(BUFFER_SIZE)(""); - - // open fifo (this will block until something writes to it) - var fd = open(path, 0); - - // read from fifo and close it - read(fd, buf, BUFFER_SIZE-1); - close(fd); - - // 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"]); - lib.close(); - return; - } - postMessage([agent, cmd, document]); - } -}; -\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/ipc.js 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 <http://www.gnu.org/licenses/>. + + ***** 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 @@ -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/pipe_worker.js b/chrome/content/zotero/xpcom/pipe_worker.js @@ -0,0 +1,65 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +onmessage = function(event) { + var path = event.data.path; + + // ctypes declarations follow + var lib = ctypes.open(event.data.libc); + + // int open(const char *path, int oflag, ...); + var open = lib.declare("open", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int); + + // ssize_t read(int fildes, void *buf, size_t nbyte); + var read = lib.declare("read", ctypes.default_abi, ctypes.ssize_t, ctypes.int, + ctypes.char.ptr, ctypes.size_t); + + // int close(int fildes); + var close = lib.declare("close", ctypes.default_abi, ctypes.int, ctypes.int); + + // define buffer for reading from fifo + const BUFFER_SIZE = 4096; + + while(true) { + var buf = ctypes.char.array(BUFFER_SIZE)(""); + + // open fifo (this will block until something writes to it) + var fd = open(path, 0); + + // read from fifo and close it + read(fd, buf, BUFFER_SIZE-1); + close(fd); + + // extract message + var string = buf.readString(); + if(string === "Zotero shutdown\n") { + postMessage(["Debug", "IPC: Worker closing "+event.data.path]); + lib.close(); + return; + } + + 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 @@ -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 @@ -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 <http://www.gnu.org/licenses/>. + + ***** 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 @@ -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 <http://www.gnu.org/licenses/>. + + ***** 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"]] = "<html>"+this._parsedPostData["html"]+"</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_firefox.js b/chrome/content/zotero/xpcom/translation/browser_firefox.js @@ -1,503 +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 <http://www.gnu.org/licenses/>. - - ***** END LICENSE BLOCK ***** -*/ - -const BOMs = { - "UTF-8":"\xEF\xBB\xBF", - "UTF-16BE":"\xFE\xFF", - "UTF-16LE":"\xFF\xFE", - "UTF-32BE":"\x00\x00\xFE\xFF", - "UTF-32LE":"\xFF\xFE\x00\x00" -} - -Components.utils.import("resource://gre/modules/NetUtil.jsm"); - -/** - * @class Manages the translator sandbox - * @param {Zotero.Translate} translate - * @param {String|window} sandboxLocation - */ -Zotero.Translate.SandboxManager = function(translate, sandboxLocation) { - this.sandbox = new Components.utils.Sandbox(sandboxLocation); - this.sandbox.Zotero = {}; - this._translate = translate; - - // import functions missing from global scope into Fx sandbox - this.sandbox.XPathResult = Components.interfaces.nsIDOMXPathResult; - this.sandbox.DOMParser = function() { - // get URI - // DEBUG: In Fx 4 we can just use document.nodePrincipal, but in Fx 3.6 this doesn't work - if(typeof sandboxLocation === "string") { // if sandbox specified by URI - var uri = sandboxLocation; - } else { // if sandbox specified by DOM document - var uri = sandboxLocation.location.toString(); - } - - // get principal from URI - 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); - uri = ioService.newURI(uri, "UTF-8", null); - var principal = secMan.getCodebasePrincipal(uri); - - // initialize DOM parser - var _DOMParser = Components.classes["@mozilla.org/xmlextras/domparser;1"] - .createInstance(Components.interfaces.nsIDOMParser); - _DOMParser.init(principal, uri, uri); - - // expose parseFromString - this.__exposedProps__ = {"parseFromString":"r"}; - this.parseFromString = function(str, contentType) _DOMParser.parseFromString(str, contentType); - } - this.sandbox.DOMParser.__exposedProps__ = {"prototype":"r"}; - this.sandbox.DOMParser.prototype = {}; -} - -Zotero.Translate.SandboxManager.prototype = { - /** - * Evaluates code in the sandbox - */ - "eval":function(code) { - Components.utils.evalInSandbox(code, 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, attachTo) { - if(!attachTo) attachTo = this.sandbox.Zotero; - var newExposedProps = false; - if(!object.__exposedProps__) newExposedProps = {}; - for(var key in (newExposedProps ? object : object.__exposedProps__)) { - let localKey = key; - if(newExposedProps) newExposedProps[localKey] = "r"; - - // magical XPCSafeJSObjectWrappers for sandbox - if(typeof object[localKey] === "function" || typeof object[localKey] === "object") { - if(attachTo == this.sandbox) Zotero.debug(localKey); - attachTo[localKey] = function() { - var args = (passAsFirstArgument ? [passAsFirstArgument] : []); - for(var i=0; i<arguments.length; i++) { - args.push((typeof arguments[i] === "object" && arguments[i] !== null) - || typeof arguments[i] === "function" - ? new XPCSafeJSObjectWrapper(arguments[i]) : arguments[i]); - } - - return object[localKey].apply(object, args); - }; - - // attach members - if(!(object instanceof Components.interfaces.nsISupports)) { - this.importObject(object[localKey], passAsFirstArgument ? passAsFirstArgument : null, attachTo[localKey]); - } - } else { - attachTo[localKey] = object[localKey]; - } - } - - if(newExposedProps) { - attachTo.__exposedProps__ = newExposedProps; - } else { - attachTo.__exposedProps__ = object.__exposedProps__; - } - } -} - -/** - * This variable holds a reference to all open nsIInputStreams and nsIOutputStreams in the global - * scope at all times. Otherwise, our streams might get garbage collected when we allow other code - * to run during Zotero.wait(). - */ -Zotero.Translate.IO.maintainedInstances = []; - -/******* (Native) Read support *******/ - -Zotero.Translate.IO.Read = function(file, mode) { - Zotero.Translate.IO.maintainedInstances.push(this); - - this.file = file; - - // open file - this._openRawStream(); - - // start detecting charset - var charset = null; - - // look for a BOM in the document - var binStream = Components.classes["@mozilla.org/binaryinputstream;1"]. - createInstance(Components.interfaces.nsIBinaryInputStream); - binStream.setInputStream(this._rawStream); - var first4 = binStream.readBytes(4); - - for(var possibleCharset in BOMs) { - if(first4.substr(0, BOMs[possibleCharset].length) == BOMs[possibleCharset]) { - this._charset = possibleCharset; - break; - } - } - - if(this._charset) { - // BOM found; store its length and go back to the beginning of the file - this._bomLength = BOMs[this._charset].length; - this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) - .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); - } else { - // look for an XML parse instruction - this._bomLength = 0; - - var sStream = Components.classes["@mozilla.org/scriptableinputstream;1"] - .createInstance(Components.interfaces.nsIScriptableInputStream); - sStream.init(this._rawStream); - - // read until we see if the file begins with a parse instruction - const whitespaceRe = /\s/g; - var read; - do { - read = sStream.read(1); - } while(whitespaceRe.test(read)) - - if(read == "<") { - var firstPart = read + sStream.read(4); - if(firstPart == "<?xml") { - // got a parse instruction, read until it ends - read = true; - while((read !== false) && (read !== ">")) { - read = sStream.read(1); - firstPart += read; - } - - const encodingRe = /encoding=['"]([^'"]+)['"]/; - var m = encodingRe.exec(firstPart); - if(m) { - try { - var charconv = Components.classes["@mozilla.org/charset-converter-manager;1"] - .getService(Components.interfaces.nsICharsetConverterManager) - .getCharsetTitle(m[1]); - if(charconv) this._charset = m[1]; - } catch(e) {} - } - - // if we know for certain document is XML, we also know for certain that the - // default charset for XML is UTF-8 - if(!this._charset) this._charset = "UTF-8"; - } - } - - // If we managed to get a charset here, then translators shouldn't be able to override it, - // since it's almost certainly correct. Otherwise, we allow override. - this._allowCharsetOverride = !!this._charset; - this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) - .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); - - if(!this._charset) { - // No XML parse instruction or BOM. - - // Check whether the user has specified a charset preference - var charsetPref = Zotero.Prefs.get("import.charset"); - if(charsetPref == "auto") { - Zotero.debug("Translate: Checking whether file is UTF-8"); - // For auto-detect, we are basically going to check if the file could be valid - // UTF-8, and if this is true, we will treat it as UTF-8. Prior likelihood of - // UTF-8 is very high, so this should be a reasonable strategy. - - // from http://codex.wordpress.org/User:Hakre/UTF8 - const UTF8Regex = new RegExp('^(?:' + - '[\x09\x0A\x0D\x20-\x7E]' + // ASCII - '|[\xC2-\xDF][\x80-\xBF]' + // non-overlong 2-byte - '|\xE0[\xA0-\xBF][\x80-\xBF]' + // excluding overlongs - '|[\xE1-\xEC\xEE][\x80-\xBF]{2}' + // 3-byte, but exclude U-FFFE and U-FFFF - '|\xEF[\x80-\xBE][\x80-\xBF]' + - '|\xEF\xBF[\x80-\xBD]' + - '|\xED[\x80-\x9F][\x80-\xBF]' + // excluding surrogates - '|\xF0[\x90-\xBF][\x80-\xBF]{2}' + // planes 1-3 - '|[\xF1-\xF3][\x80-\xBF]{3}' + // planes 4-15 - '|\xF4[\x80-\x8F][\x80-\xBF]{2}' + // plane 16 - ')*$'); - - // Read all currently available bytes from file. This seems to be the entire file, - // since the IO is blocking anyway. - this._charset = "UTF-8"; - let bytesAvailable; - while(bytesAvailable = this._rawStream.available()) { - // read 131072 bytes - let fileContents = binStream.readBytes(Math.min(131072, bytesAvailable)); - - // on failure, try reading up to 3 more bytes and see if that makes this - // valid (since we have chunked it) - let isUTF8; - for(let i=1; !(isUTF8 = UTF8Regex.test(fileContents)) && i <= 3; i++) { - if(this._rawStream.available()) { - fileContents += binStream.readBytes(1); - } - } - - // if the regexp continues to fail, this is not UTF-8 - if(!isUTF8) { - // Can't be UTF-8; see if a default charset is defined - this._charset = Zotero.Prefs.get("intl.charset.default", true); - - // ISO-8859-1 by default - if(!this._charset) this._charset = "ISO-8859-1"; - - break; - } - } - } else { - // No need to auto-detect; user has specified a charset - this._charset = charsetPref; - } - } - } - - Zotero.debug("Translate: Detected file charset as "+this._charset); - - // We know the charset now. Open a converter stream. - if(mode) this.reset(mode); -} - -Zotero.Translate.IO.Read.prototype = { - "__exposedProps__":{ - "_getXML":"r", - "RDF":"r", - "read":"r", - "setCharacterSet":"r" - }, - - "_openRawStream":function() { - if(this._rawStream) this._rawStream.close(); - this._rawStream = Components.classes["@mozilla.org/network/file-input-stream;1"] - .createInstance(Components.interfaces.nsIFileInputStream); - this._rawStream.init(this.file, 0x01, 0664, 0); - }, - - "_seekToStart":function(charset) { - this._openRawStream(); - - this._linesExhausted = false; - this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) - .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); - this.bytesRead = this._bomLength; - - this.inputStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Components.interfaces.nsIConverterInputStream); - this.inputStream.init(this._rawStream, charset, 32768, - Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); - }, - - "_readToString":function() { - var str = {}; - var stringBits = []; - this.inputStream.QueryInterface(Components.interfaces.nsIUnicharInputStream); - while(1) { - var read = this.inputStream.readString(32768, str); - if(!read) break; - stringBits.push(str.value); - } - return stringBits.join(""); - }, - - "_initRDF":function() { - // call Zotero.wait() to do UI repaints - Zotero.wait(); - - // get URI - var IOService = Components.classes['@mozilla.org/network/io-service;1'] - .getService(Components.interfaces.nsIIOService); - var fileHandler = IOService.getProtocolHandler("file") - .QueryInterface(Components.interfaces.nsIFileProtocolHandler); - var baseURI = fileHandler.getURLSpecFromFile(this.file); - - Zotero.debug("Translate: Initializing RDF data store"); - this._dataStore = new Zotero.RDF.AJAW.RDFIndexedFormula(); - var parser = new Zotero.RDF.AJAW.RDFParser(this._dataStore); - try { - var nodes = Zotero.Translate.IO.parseDOMXML(this._rawStream, this._charset, this.file.fileSize); - parser.parse(nodes, baseURI); - - this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore); - } catch(e) { - this.close(); - throw "Translate: No RDF found"; - } - }, - - "setCharacterSet":function(charset) { - if(typeof charset !== "string") { - throw "Translate: setCharacterSet: charset must be a string"; - } - - // seek back to the beginning - this._seekToStart(this._allowCharsetOverride ? this._allowCharsetOverride : this._charset); - - if(!_allowCharsetOverride) { - Zotero.debug("Translate: setCharacterSet: translate charset override ignored due to BOM or XML parse instruction"); - } - }, - - "read":function(bytes) { - var str = {}; - - if(bytes) { - // read number of bytes requested - this.inputStream.QueryInterface(Components.interfaces.nsIUnicharInputStream); - var amountRead = this.inputStream.readString(bytes, str); - if(!amountRead) return false; - this.bytesRead += amountRead; - } else { - // bytes not specified; read a line - this.inputStream.QueryInterface(Components.interfaces.nsIUnicharLineInputStream); - if(this._linesExhausted) return false; - this._linesExhausted = !this.inputStream.readLine(str); - this.bytesRead += str.value.length+1; // only approximate - } - - return str.value; - }, - - "_getXML":function() { - if(this._mode == "xml/dom") { - return Zotero.Translate.IO.parseDOMXML(this._rawStream, this._charset, this.file.fileSize); - } else { - return this._readToString().replace(/<\?xml[^>]+\?>/, ""); - } - }, - - "reset":function(newMode) { - if(Zotero.Translate.IO.maintainedInstances.indexOf(this) === -1) { - Zotero.Translate.IO.maintainedInstances.push(this); - } - this._seekToStart(this._charset); - - this._mode = newMode; - if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && !this.RDF) { - this._initRDF(); - } - }, - - "close":function() { - var myIndex = Zotero.Translate.IO.maintainedInstances.indexOf(this); - if(myIndex !== -1) Zotero.Translate.IO.maintainedInstances.splice(myIndex, 1); - - if(this._rawStream) { - this._rawStream.close(); - delete this._rawStream; - } - } -} -Zotero.Translate.IO.Read.prototype.__defineGetter__("contentLength", -function() { - return this.file.fileSize; -}); - -/******* Write support *******/ - -Zotero.Translate.IO.Write = function(file, mode, charset) { - Zotero.Translate.IO.maintainedInstances.push(this); - this._rawStream = Components.classes["@mozilla.org/network/file-output-stream;1"] - .createInstance(Components.interfaces.nsIFileOutputStream); - this._rawStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate - this._writtenToStream = false; - if(mode || charset) this.reset(mode, charset); -} - -Zotero.Translate.IO.Write.prototype = { - "__exposedProps__":{ - "RDF":"r", - "write":"r", - "setCharacterSet":"r" - }, - - "_initRDF":function() { - Zotero.debug("Translate: Initializing RDF data store"); - this._dataStore = new Zotero.RDF.AJAW.RDFIndexedFormula(); - this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore); - }, - - "setCharacterSet":function(charset) { - if(typeof charset !== "string") { - throw "Translate: setCharacterSet: charset must be a string"; - } - - if(!this.outputStream) { - this.outputStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] - .createInstance(Components.interfaces.nsIConverterOutputStream); - } - - if(charset == "UTF-8xBOM") charset = "UTF-8"; - this.outputStream.init(this._rawStream, charset, 1024, "?".charCodeAt(0)); - this._charset = charset; - }, - - "write":function(data) { - if(!this._charset) this.setCharacterSet("UTF-8"); - - if(!this._writtenToStream && this._charset.substr(this._charset.length-4) == "xBOM" - && BOMs[this._charset.substr(0, this._charset.length-4).toUpperCase()]) { - // If stream has not yet been written to, and a UTF type has been selected, write BOM - this._rawStream.write(BOMs[streamCharset], BOMs[streamCharset].length); - } - - if(this._charset == "MACINTOSH") { - // fix buggy Mozilla MacRoman - var splitData = data.split(/([\r\n]+)/); - for(var i=0; i<splitData.length; i+=2) { - // write raw newlines straight to the string - this.outputStream.writeString(splitData[i]); - if(splitData[i+1]) { - this._rawStream.write(splitData[i+1], splitData[i+1].length); - } - } - } else { - this.outputStream.writeString(data); - } - - this._writtenToStream = true; - }, - - "reset":function(newMode, charset) { - this._mode = newMode; - if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) { - this._initRDF(); - if(!this._writtenToString) this.setCharacterSet("UTF-8"); - } else if(!this._writtenToString) { - this.setCharacterSet(charset ? charset : "UTF-8"); - } - }, - - "close":function() { - if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) { - this.write(this.RDF.serialize()); - } - - var myIndex = Zotero.Translate.IO.maintainedInstances.indexOf(this); - if(myIndex !== -1) Zotero.Translate.IO.maintainedInstances.splice(myIndex, 1); - - this._rawStream.close(); - } -} diff --git a/chrome/content/zotero/xpcom/translation/browser_other.js b/chrome/content/zotero/xpcom/translation/browser_other.js @@ -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 <http://www.gnu.org/licenses/>. - - ***** 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<arguments.length; i++) args.push(arguments[i]); - fn.apply(object, args); - }; - } - } -} -\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/item_connector.js b/chrome/content/zotero/xpcom/translation/item_connector.js @@ -1,30 +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 <http://www.gnu.org/licenses/>. - - ***** END LICENSE BLOCK ***** -*/ - -Zotero.Translate.Item = { - "saveItem":function (translate, item) { - - } -} -\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/tlds.js b/chrome/content/zotero/xpcom/translation/tlds.js @@ -0,0 +1,271 @@ +const TLDS = { + "ac":true, + "ad":true, + "ae":true, + "aero":true, + "af":true, + "ag":true, + "ai":true, + "al":true, + "am":true, + "an":true, + "ao":true, + "aq":true, + "ar":true, + "arpa":true, + "as":true, + "asia":true, + "at":true, + "au":true, + "aw":true, + "ax":true, + "az":true, + "ba":true, + "bb":true, + "bd":true, + "be":true, + "bf":true, + "bg":true, + "bh":true, + "bi":true, + "biz":true, + "bj":true, + "bm":true, + "bn":true, + "bo":true, + "br":true, + "bs":true, + "bt":true, + "bv":true, + "bw":true, + "by":true, + "bz":true, + "ca":true, + "cat":true, + "cc":true, + "cd":true, + "cf":true, + "cg":true, + "ch":true, + "ci":true, + "ck":true, + "cl":true, + "cm":true, + "cn":true, + "co":true, + "com":true, + "coop":true, + "cr":true, + "cu":true, + "cv":true, + "cx":true, + "cy":true, + "cz":true, + "de":true, + "dj":true, + "dk":true, + "dm":true, + "do":true, + "dz":true, + "ec":true, + "edu":true, + "ee":true, + "eg":true, + "er":true, + "es":true, + "et":true, + "eu":true, + "fi":true, + "fj":true, + "fk":true, + "fm":true, + "fo":true, + "fr":true, + "ga":true, + "gb":true, + "gd":true, + "ge":true, + "gf":true, + "gg":true, + "gh":true, + "gi":true, + "gl":true, + "gm":true, + "gn":true, + "gov":true, + "gp":true, + "gq":true, + "gr":true, + "gs":true, + "gt":true, + "gu":true, + "gw":true, + "gy":true, + "hk":true, + "hm":true, + "hn":true, + "hr":true, + "ht":true, + "hu":true, + "id":true, + "ie":true, + "il":true, + "im":true, + "in":true, + "info":true, + "int":true, + "io":true, + "iq":true, + "ir":true, + "is":true, + "it":true, + "je":true, + "jm":true, + "jo":true, + "jobs":true, + "jp":true, + "ke":true, + "kg":true, + "kh":true, + "ki":true, + "km":true, + "kn":true, + "kp":true, + "kr":true, + "kw":true, + "ky":true, + "kz":true, + "la":true, + "lb":true, + "lc":true, + "li":true, + "lk":true, + "lr":true, + "ls":true, + "lt":true, + "lu":true, + "lv":true, + "ly":true, + "ma":true, + "mc":true, + "md":true, + "me":true, + "mg":true, + "mh":true, + "mil":true, + "mk":true, + "ml":true, + "mm":true, + "mn":true, + "mo":true, + "mobi":true, + "mp":true, + "mq":true, + "mr":true, + "ms":true, + "mt":true, + "mu":true, + "museum":true, + "mv":true, + "mw":true, + "mx":true, + "my":true, + "mz":true, + "na":true, + "name":true, + "nc":true, + "ne":true, + "net":true, + "nf":true, + "ng":true, + "ni":true, + "nl":true, + "no":true, + "np":true, + "nr":true, + "nu":true, + "nz":true, + "om":true, + "org":true, + "pa":true, + "pe":true, + "pf":true, + "pg":true, + "ph":true, + "pk":true, + "pl":true, + "pm":true, + "pn":true, + "pr":true, + "pro":true, + "ps":true, + "pt":true, + "pw":true, + "py":true, + "qa":true, + "re":true, + "ro":true, + "rs":true, + "ru":true, + "rw":true, + "sa":true, + "sb":true, + "sc":true, + "sd":true, + "se":true, + "sg":true, + "sh":true, + "si":true, + "sj":true, + "sk":true, + "sl":true, + "sm":true, + "sn":true, + "so":true, + "sr":true, + "st":true, + "su":true, + "sv":true, + "sy":true, + "sz":true, + "tc":true, + "td":true, + "tel":true, + "tf":true, + "tg":true, + "th":true, + "tj":true, + "tk":true, + "tl":true, + "tm":true, + "tn":true, + "to":true, + "tp":true, + "tr":true, + "travel":true, + "tt":true, + "tv":true, + "tw":true, + "tz":true, + "ua":true, + "ug":true, + "uk":true, + "us":true, + "uy":true, + "uz":true, + "va":true, + "vc":true, + "ve":true, + "vg":true, + "vi":true, + "vn":true, + "vu":true, + "wf":true, + "ws":true, + "xxx":true, + "ye":true, + "yt":true, + "za":true, + "zm":true, + "zw":true +}; +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js @@ -25,14 +25,15 @@ /** * @class - * Deprecated class for creating new Zotero.Translate instances + * Deprecated class for creating new Zotero.Translate instances<br/> + * <br/> * 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); + 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."; + } + } - 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<arguments.length; i++) { + args.push(arguments[i]); + } + for(var i in this._handlers[type]) { Zotero.debug("Translate: running handler "+i+" for "+type, 5); try { - if(this._parentTranslator) { - returnValue = this._handlers[type][i](null, argument); - } else { - returnValue = this._handlers[type][i](this, argument); - } + returnValue = this._handlers[type][i].apply(null, args); } catch(e) { if(this._parentTranslator) { // throw handler errors if they occur when a translator is @@ -635,16 +706,73 @@ Zotero.Translate.Base.prototype = { if(this._currentState == "detect") throw "Translate: getTranslators: detection is already running"; this._currentState = "detect"; this._getAllTranslators = getAllTranslators; - this._potentialTranslators = this._getPotentialTranslators(); + this._getTranslatorsGetPotentialTranslators(); + + // if detection returns immediately, return found translators + if(!this._currentState) return this._foundTranslators; + }, + + /** + * Get all potential translators + * @return {Zotero.Translator[]} + */ + "_getTranslatorsGetPotentialTranslators":function() { + var me = this; + Zotero.Translators.getAllForType(this.type, + function(translators) { me._getTranslatorsTranslatorsReceived(translators) }); + }, + + /** + * Called on completion of {@link #_getTranslatorsGetPotentialTranslators} call + */ + "_getTranslatorsTranslatorsReceived":function(allPotentialTranslators, properToProxyFunctions) { + this._potentialTranslators = []; this._foundTranslators = []; - Zotero.debug("Translate: Searching for translators for "+(this.path ? this.path : "an undisclosed location"), 3); + // this gets passed out by Zotero.Translators.getWebTranslatorsForLocation() because it is + // specific for each translator, but we want to avoid making a copy of a translator whenever + // possible. + this._properToProxyFunctions = properToProxyFunctions ? properToProxyFunctions : null; + this._waitingForRPC = false; - this._detect(); + for(var i in allPotentialTranslators) { + var translator = allPotentialTranslators[i]; + if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { + this._potentialTranslators.push(translator); + } else { + this._waitingForRPC = true; + } + } - // if detection returns immediately, return found translators - if(!this._currentState) return this._foundTranslators; + // TODO maybe this should only be in the web translator + if(this._waitingForRPC) { + var me = this; + Zotero.Connector.callMethod("detect", {"uri":this.location.toString(), + "cookie":this.document.cookie, + "html":this.document.documentElement.innerHTML}, + function(returnValue) { me._getTranslatorsRPCComplete(returnValue) }); + } + + this._detect(); }, + + /** + * Called on completion of detect RPC for + * {@link Zotero.Translate.Base#_getTranslatorsTranslatorsReceived} + */ + "_getTranslatorsRPCComplete":function(rpcTranslators) { + this._waitingForRPC = false; + + // if there are translators, add them to the list of found translators + if(rpcTranslators) { + this._foundTranslators = this._foundTranslators.concat(rpcTranslators); + } + + // call _detectTranslatorsCollected to return detected translators + if(this._currentState === null) { + this._detectTranslatorsCollected(); + } + }, /** * Begins the actual translation. At present, this returns immediately for import/export @@ -656,14 +784,34 @@ Zotero.Translate.Base.prototype = { * if FALSE, don't save items * @param {Boolean} [saveAttachments=true] Exclude attachments (e.g., snapshots) on import */ - "translate":function(libraryID, saveAttachments) { - // initialize properties specific to each translation + "translate":function(libraryID, saveAttachments) { // initialize properties specific to each translation this._currentState = "translate"; if(!this.translator || !this.translator.length) { throw("Translate: Failed: no translator specified"); } + this._libraryID = libraryID; + this._saveAttachments = saveAttachments === undefined || saveAttachments; + + if(typeof this.translator[0] === "object") { + // already have a translator object, so use it + this._translateHaveTranslator(); + } else { + // need to get translator first + var me = this; + Zotero.Translators.get(this.translator[0], + function(translator) { + me.translator[0] = translator; + me._translateHaveTranslator(); + }); + } + }, + + /** + * Called when translator has been retrieved + */ + "_translateHaveTranslator":function() { // load translators if(!this._loadTranslator(this.translator[0])) return; @@ -671,15 +819,13 @@ Zotero.Translate.Base.prototype = { if(!this._displayOptions) this._displayOptions = this.translator[0].displayOptions; // prepare translation - this._libraryID = libraryID; - this._saveAttachments = typeof saveAttachments === "undefined" ? true : saveAttachments; this._prepareTranslation(); Zotero.debug("Translate: Beginning translation with "+this.translator[0].label); // translate try { - this._sandboxManager.sandbox["do"+this._entryFunctionSuffix].apply(this.null, this._getParameters()); + this._sandboxManager.sandbox["do"+this._entryFunctionSuffix].apply(null, this._getParameters()); } catch(e) { if(this._parentTranslator) { throw(e); @@ -715,9 +861,10 @@ Zotero.Translate.Base.prototype = { if(oldState === "detect") { if(this._potentialTranslators.length) { var lastTranslator = this._potentialTranslators.shift(); + var lastProperToProxyFunction = this._properToProxyFunctions ? this._properToProxyFunctions.shift() : null; if(returnValue) { - var dupeTranslator = {"itemType":returnValue}; + var dupeTranslator = {"itemType":returnValue, "properToProxy":lastProperToProxyFunction}; for(var i in lastTranslator) dupeTranslator[i] = lastTranslator[i]; this._foundTranslators.push(dupeTranslator); } else if(error) { @@ -730,7 +877,7 @@ Zotero.Translate.Base.prototype = { this._detect(); } else { this._currentState = null; - this._runHandler("translators", this._foundTranslators ? this._foundTranslators : false); + if(!this._waitingForRPC) this._detectTranslatorsCollected(); } } else { this._currentState = null; @@ -762,6 +909,12 @@ Zotero.Translate.Base.prototype = { * Runs detect code for a translator */ "_detect":function() { + // there won't be any translators if we need an RPC call + if(!this._potentialTranslators.length) { + this.complete(true); + return; + } + if(!this._loadTranslator(this._potentialTranslators[0])) { this.complete(false, "Error loading translator into sandbox"); return; @@ -779,6 +932,15 @@ Zotero.Translate.Base.prototype = { }, /** + * Called when all translators have been collected for detection + */ + "_detectTranslatorsCollected":function() { + Zotero.debug("Translate: All translator detect calls and RPC calls complete"); + this._foundTranslators.sort(function(a, b) { return a.priority-b.priority }); + this._runHandler("translators", this._foundTranslators); + }, + + /** * Loads the translator into its sandbox * @param {Zotero.Translator} translator * @return {Boolean} Whether the translator could be successfully loaded @@ -795,7 +957,6 @@ Zotero.Translate.Base.prototype = { try { this._sandboxManager.eval("var translatorInfo = "+translator.code, this._sandbox); - return true; } catch(e) { if(translator.logError) { translator.logError(e.toString()); @@ -815,13 +976,14 @@ Zotero.Translate.Base.prototype = { */ "_generateSandbox":function() { Zotero.debug("Translate: Binding sandbox to "+(typeof this._sandboxLocation == "object" ? this._sandboxLocation.document.location : this._sandboxLocation), 4); - this._sandboxManager = new Zotero.Translate.SandboxManager(this, this._sandboxLocation); + this._sandboxManager = new Zotero.Translate.SandboxManager(this._sandboxLocation); const createArrays = "['creators', 'notes', 'tags', 'seeAlso', 'attachments']"; var src = "var Zotero = {};"+ "Zotero.Item = function (itemType) {"+ + "const createArrays = "+createArrays+";"+ "this.itemType = itemType;"+ - "for each(var array in "+createArrays+") {"+ - "this[array] = [];"+ + "for(var i in createArrays) {"+ + "this[createArrays[i]] = [];"+ "}"+ "};"+ "Zotero.Collection = function () {};"+ @@ -927,17 +1089,11 @@ Zotero.Translate.Base.prototype = { * No-op for preparing translation */ "_prepareTranslation":function() {}, - - /** - * Get all potential translators - * @return {Zotero.Translator[]} - */ - "_getPotentialTranslators":function() { - return Zotero.Translators.getAllForType(this.type); - } } /** + * @class Web translation + * * @property {Document} document The document object to be used for web scraping (set with setDocument) * @property {Zotero.Connector.CookieManager} cookieManager A CookieManager to manage cookies for * this Translate instance. @@ -975,30 +1131,21 @@ Zotero.Translate.Web.prototype.setCookieManager = function(cookieManager) { * @param {String} location The URL of the page to translate */ Zotero.Translate.Web.prototype.setLocation = function(location) { - // account for proxies - this.location = Zotero.Proxies.proxyToProper(location); - if(this.location != location) { - // figure out if this URL is being proxies - this.locationIsProxied = true; - } + this.location = location; this.path = this.location; } /** - * Get all potential translators + * Get potential web translators */ -Zotero.Translate.Web.prototype._getPotentialTranslators = function() { - var allTranslators = Zotero.Translators.getAllForType("web"); - var potentialTranslators = []; - - Zotero.debug("Translate: Running regular expressions"); - for(var i=0; i<allTranslators.length; i++) { - if(!allTranslators[i].webRegexp || (this.location.length < 8192 && allTranslators[i].webRegexp.test(this.location))) { - potentialTranslators.push(allTranslators[i]); - } - } - - return potentialTranslators; +Zotero.Translate.Web.prototype._getTranslatorsGetPotentialTranslators = function() { + var me = this; + Zotero.Translators.getWebTranslatorsForLocation(this.location, + function(data) { + // data[0] = list of translators + // data[1] = list of functions to convert proper URIs to proxied URIs + me._getTranslatorsTranslatorsReceived(data[0], data[1]); + }); } /** @@ -1023,14 +1170,67 @@ Zotero.Translate.Web.prototype._prepareTranslation = function() { } /** - * Overload detect to test regexp first + * Overload translate to set selectedItems + */ +Zotero.Translate.Web.prototype.translate = function(libraryID, saveAttachments, selectedItems) { + this._selectedItems = selectedItems; + Zotero.Translate.Base.prototype.translate.apply(this, libraryID, saveAttachments); +} + +/** + * Overload _translateHaveTranslator to send an RPC call if necessary + */ +Zotero.Translate.Web.prototype._translateHaveTranslator = function() { + if(this.translator[0].runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { + // begin process to run translator in browser + Zotero.Translate.Base.prototype._translateHaveTranslator.apply(this); + } else { + // otherwise, ferry translator load to RPC + var me = this; + Zotero.Connector.callMethod("savePage", { + "uri":this.location.toString(), + "translatorID":(typeof this.translator[0] === "object" + ? this.translator[0].translatorID : this.translator[0]), + "cookie":this.document.cookie, + "html":this.document.documentElement.innerHTML + }, function(obj) { me._translateRPCComplete(obj) }); + } +} + +/** + * Called when an RPC call for remote translation completes + */ +Zotero.Translate.Web.prototype._translateRPCComplete = function(obj, failureCode) { + if(!obj) this.complete(false, failureCode); + + if(obj.selectItems) { + // if we have to select items, call the selectItems handler and do it + var me = this; + var items = this._runHandler("select", obj.selectItems, + function(selectedItems) { + Zotero.Connector.callMethod("selectItems", + {"instanceID":obj.instanceID, "selectedItems":selectedItems}, + function(obj) { me._translateRPCComplete(obj) }) + } + ); + } else { + // if we don't have to select items, continue + for(var i in obj.items) { + this._runHandler("itemDone", null, obj.items[i]); + } + this.complete(true); + } +} + +/** + * Overload complete to report translation failure */ Zotero.Translate.Web.prototype.complete = function(returnValue, error) { // call super var oldState = this._currentState; var errorString = Zotero.Translate.Base.prototype.complete.apply(this, [returnValue, error]); - // Report translaton failure if we failed + // Report translation failure if we failed if(oldState == "translate" && errorString && this.translator[0].inRepository && Zotero.Prefs.get("reportTranslationFailure")) { // Don't report failure if in private browsing mode if(Zotero.isFx && !Zotero.isStandalone) { @@ -1049,6 +1249,9 @@ Zotero.Translate.Web.prototype.complete = function(returnValue, error) { } } +/** + * @class Import translation + */ Zotero.Translate.Import = function() { this.init(); } @@ -1081,35 +1284,26 @@ Zotero.Translate.Import.prototype.complete = function(returnValue, error) { } /** - * Get all potential translators, ordering translators with the right file extension first + * Get all potential import translators, ordering translators with the right file extension first */ -Zotero.Translate.Import.prototype._getPotentialTranslators = function() { - var allTranslators = Zotero.Translators.getAllForType("import"); - var tier1Translators = []; - var tier2Translators = []; - - for(var i=0; i<allTranslators.length; i++) { - if(allTranslators[i].importRegexp.test(this.location)) { - tier1Translators.push(allTranslators[i]); - } else { - tier2Translators.push(allTranslators[i]); - } - } - - return tier1Translators.concat(tier2Translators); +Zotero.Translate.Import.prototype._getTranslatorsGetPotentialTranslators = function() { + var me = this; + Zotero.Translators.getImportTranslatorsForLocation(this.location, + function(translators) { me._getTranslatorsTranslatorsReceived(translators) }); } /** - * Overload Zotero.Translate.Base#_detect to return all translators immediately only if no string - * or location is set + * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately only + * if no string or location is set */ -Zotero.Translate.Import.prototype._detect = function() { +Zotero.Translate.Import.prototype.getTranslators = function() { if(!this._string && !this.location) { - this._foundTranslators = this._potentialTranslators; + this._foundTranslators = Zotero.Translators.getAllForType(this.type); this._potentialTranslators = []; this.complete(true); + return this._foundTranslators; } else { - Zotero.Translate.Base.prototype._detect.call(this); + Zotero.Translate.Base.prototype.getTranslators.call(this); } } @@ -1156,7 +1350,7 @@ Zotero.Translate.Import.prototype._loadTranslator = function(translator) { this._sandboxManager.importObject(this._io); return true; -}, +} /** * Prepare translation @@ -1182,6 +1376,9 @@ function() { }); +/** + * @class Export translation + */ Zotero.Translate.Export = function() { this.init(); } @@ -1235,12 +1432,13 @@ Zotero.Translate.Export.prototype.setDisplayOptions = function(displayOptions) { Zotero.Translate.Export.prototype.complete = Zotero.Translate.Import.prototype.complete; /** - * Overload Zotero.Translate.Base#_detect to return all translators immediately + * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately */ -Zotero.Translate.Export.prototype._detect = function() { - this._foundTranslators = this._potentialTranslators; +Zotero.Translate.Export.prototype.getTranslators = function() { + this._foundTranslators = Zotero.Translators.getAllForType(this.type); this._potentialTranslators = []; this.complete(true); + return this._foundTranslators; } /** @@ -1295,6 +1493,7 @@ function() { }); /** + * @class Search translation * @property {Array[]} search Item (in {@link Zotero.Item#serialize} format) to extrapolate data * (set with setSearch) */ @@ -1307,7 +1506,7 @@ Zotero.Translate.Search.prototype._entryFunctionSuffix = "Search"; Zotero.Translate.Search.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Search); /** - * @borrows Zotero.Translate.Web#setCookieManager as Zotero.Translate.Search#setCookieManager + * @borrows Zotero.Translate.Web#setCookieManager */ Zotero.Translate.Search.prototype.setCookieManager = Zotero.Translate.Web.prototype.setCookieManager; @@ -1339,11 +1538,7 @@ Zotero.Translate.Search.prototype.setTranslator = function(translator) { // accept a list of objects this.translator = []; for(var i in translator) { - if(typeof(translator[i]) == "object") { - this.translator.push(translator[i]); - } else { - this.translator.push(Zotero.Translators.get(translator[i])); - } + this.translator.push(translator[i]); } return true; } else { @@ -1436,6 +1631,9 @@ Zotero.Translate.IO = { /******* String support *******/ +/** + * @class Translate backend for translating from a string + */ Zotero.Translate.IO.String = function(string, uri, mode) { if(string && typeof string === "string") { this._string = string; diff --git a/chrome/content/zotero/xpcom/translation/translate_firefox.js b/chrome/content/zotero/xpcom/translation/translate_firefox.js @@ -0,0 +1,503 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +const BOMs = { + "UTF-8":"\xEF\xBB\xBF", + "UTF-16BE":"\xFE\xFF", + "UTF-16LE":"\xFF\xFE", + "UTF-32BE":"\x00\x00\xFE\xFF", + "UTF-32LE":"\xFF\xFE\x00\x00" +} + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +/** + * @class Manages the translator sandbox + * @param {Zotero.Translate} translate + * @param {String|window} sandboxLocation + */ +Zotero.Translate.SandboxManager = function(sandboxLocation) { + this.sandbox = new Components.utils.Sandbox(sandboxLocation); + this.sandbox.Zotero = {}; + + // import functions missing from global scope into Fx sandbox + this.sandbox.XPathResult = Components.interfaces.nsIDOMXPathResult; + this.sandbox.DOMParser = function() { + // get URI + // DEBUG: In Fx 4 we can just use document.nodePrincipal, but in Fx 3.6 this doesn't work + if(typeof sandboxLocation === "string") { // if sandbox specified by URI + var uri = sandboxLocation; + } else { // if sandbox specified by DOM document + var uri = sandboxLocation.location.toString(); + } + + // get principal from URI + 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); + uri = ioService.newURI(uri, "UTF-8", null); + var principal = secMan.getCodebasePrincipal(uri); + + // initialize DOM parser + var _DOMParser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + _DOMParser.init(principal, uri, uri); + + // expose parseFromString + this.__exposedProps__ = {"parseFromString":"r"}; + this.parseFromString = function(str, contentType) _DOMParser.parseFromString(str, contentType); + } + this.sandbox.DOMParser.__exposedProps__ = {"prototype":"r"}; + this.sandbox.DOMParser.prototype = {}; +} + +Zotero.Translate.SandboxManager.prototype = { + /** + * Evaluates code in the sandbox + */ + "eval":function(code) { + Components.utils.evalInSandbox(code, 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, attachTo) { + if(!attachTo) attachTo = this.sandbox.Zotero; + var newExposedProps = false; + if(!object.__exposedProps__) newExposedProps = {}; + for(var key in (newExposedProps ? object : object.__exposedProps__)) { + let localKey = key; + if(newExposedProps) newExposedProps[localKey] = "r"; + + // magical XPCSafeJSObjectWrappers for sandbox + if(typeof object[localKey] === "function" || typeof object[localKey] === "object") { + if(attachTo == this.sandbox) Zotero.debug(localKey); + attachTo[localKey] = function() { + var args = (passAsFirstArgument ? [passAsFirstArgument] : []); + for(var i=0; i<arguments.length; i++) { + args.push((typeof arguments[i] === "object" && arguments[i] !== null) + || typeof arguments[i] === "function" + ? new XPCSafeJSObjectWrapper(arguments[i]) : arguments[i]); + } + + return object[localKey].apply(object, args); + }; + attachTo[localKey].name = localKey; + + // attach members + if(!(object instanceof Components.interfaces.nsISupports)) { + this.importObject(object[localKey], passAsFirstArgument ? passAsFirstArgument : null, attachTo[localKey]); + } + } else { + attachTo[localKey] = object[localKey]; + } + } + + if(newExposedProps) { + attachTo.__exposedProps__ = newExposedProps; + } else { + attachTo.__exposedProps__ = object.__exposedProps__; + } + } +} + +/** + * This variable holds a reference to all open nsIInputStreams and nsIOutputStreams in the global + * scope at all times. Otherwise, our streams might get garbage collected when we allow other code + * to run during Zotero.wait(). + */ +Zotero.Translate.IO.maintainedInstances = []; + +/******* (Native) Read support *******/ + +Zotero.Translate.IO.Read = function(file, mode) { + Zotero.Translate.IO.maintainedInstances.push(this); + + this.file = file; + + // open file + this._openRawStream(); + + // start detecting charset + var charset = null; + + // look for a BOM in the document + var binStream = Components.classes["@mozilla.org/binaryinputstream;1"]. + createInstance(Components.interfaces.nsIBinaryInputStream); + binStream.setInputStream(this._rawStream); + var first4 = binStream.readBytes(4); + + for(var possibleCharset in BOMs) { + if(first4.substr(0, BOMs[possibleCharset].length) == BOMs[possibleCharset]) { + this._charset = possibleCharset; + break; + } + } + + if(this._charset) { + // BOM found; store its length and go back to the beginning of the file + this._bomLength = BOMs[this._charset].length; + this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) + .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); + } else { + // look for an XML parse instruction + this._bomLength = 0; + + var sStream = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + sStream.init(this._rawStream); + + // read until we see if the file begins with a parse instruction + const whitespaceRe = /\s/g; + var read; + do { + read = sStream.read(1); + } while(whitespaceRe.test(read)) + + if(read == "<") { + var firstPart = read + sStream.read(4); + if(firstPart == "<?xml") { + // got a parse instruction, read until it ends + read = true; + while((read !== false) && (read !== ">")) { + read = sStream.read(1); + firstPart += read; + } + + const encodingRe = /encoding=['"]([^'"]+)['"]/; + var m = encodingRe.exec(firstPart); + if(m) { + try { + var charconv = Components.classes["@mozilla.org/charset-converter-manager;1"] + .getService(Components.interfaces.nsICharsetConverterManager) + .getCharsetTitle(m[1]); + if(charconv) this._charset = m[1]; + } catch(e) {} + } + + // if we know for certain document is XML, we also know for certain that the + // default charset for XML is UTF-8 + if(!this._charset) this._charset = "UTF-8"; + } + } + + // If we managed to get a charset here, then translators shouldn't be able to override it, + // since it's almost certainly correct. Otherwise, we allow override. + this._allowCharsetOverride = !!this._charset; + this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) + .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); + + if(!this._charset) { + // No XML parse instruction or BOM. + + // Check whether the user has specified a charset preference + var charsetPref = Zotero.Prefs.get("import.charset"); + if(charsetPref == "auto") { + Zotero.debug("Translate: Checking whether file is UTF-8"); + // For auto-detect, we are basically going to check if the file could be valid + // UTF-8, and if this is true, we will treat it as UTF-8. Prior likelihood of + // UTF-8 is very high, so this should be a reasonable strategy. + + // from http://codex.wordpress.org/User:Hakre/UTF8 + const UTF8Regex = new RegExp('^(?:' + + '[\x09\x0A\x0D\x20-\x7E]' + // ASCII + '|[\xC2-\xDF][\x80-\xBF]' + // non-overlong 2-byte + '|\xE0[\xA0-\xBF][\x80-\xBF]' + // excluding overlongs + '|[\xE1-\xEC\xEE][\x80-\xBF]{2}' + // 3-byte, but exclude U-FFFE and U-FFFF + '|\xEF[\x80-\xBE][\x80-\xBF]' + + '|\xEF\xBF[\x80-\xBD]' + + '|\xED[\x80-\x9F][\x80-\xBF]' + // excluding surrogates + '|\xF0[\x90-\xBF][\x80-\xBF]{2}' + // planes 1-3 + '|[\xF1-\xF3][\x80-\xBF]{3}' + // planes 4-15 + '|\xF4[\x80-\x8F][\x80-\xBF]{2}' + // plane 16 + ')*$'); + + // Read all currently available bytes from file. This seems to be the entire file, + // since the IO is blocking anyway. + this._charset = "UTF-8"; + let bytesAvailable; + while(bytesAvailable = this._rawStream.available()) { + // read 131072 bytes + let fileContents = binStream.readBytes(Math.min(131072, bytesAvailable)); + + // on failure, try reading up to 3 more bytes and see if that makes this + // valid (since we have chunked it) + let isUTF8; + for(let i=1; !(isUTF8 = UTF8Regex.test(fileContents)) && i <= 3; i++) { + if(this._rawStream.available()) { + fileContents += binStream.readBytes(1); + } + } + + // if the regexp continues to fail, this is not UTF-8 + if(!isUTF8) { + // Can't be UTF-8; see if a default charset is defined + this._charset = Zotero.Prefs.get("intl.charset.default", true); + + // ISO-8859-1 by default + if(!this._charset) this._charset = "ISO-8859-1"; + + break; + } + } + } else { + // No need to auto-detect; user has specified a charset + this._charset = charsetPref; + } + } + } + + Zotero.debug("Translate: Detected file charset as "+this._charset); + + // We know the charset now. Open a converter stream. + if(mode) this.reset(mode); +} + +Zotero.Translate.IO.Read.prototype = { + "__exposedProps__":{ + "_getXML":"r", + "RDF":"r", + "read":"r", + "setCharacterSet":"r" + }, + + "_openRawStream":function() { + if(this._rawStream) this._rawStream.close(); + this._rawStream = Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + this._rawStream.init(this.file, 0x01, 0664, 0); + }, + + "_seekToStart":function(charset) { + this._openRawStream(); + + this._linesExhausted = false; + this._rawStream.QueryInterface(Components.interfaces.nsISeekableStream) + .seek(Components.interfaces.nsISeekableStream.NS_SEEK_SET, this._bomLength); + this.bytesRead = this._bomLength; + + this.inputStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Components.interfaces.nsIConverterInputStream); + this.inputStream.init(this._rawStream, charset, 32768, + Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + }, + + "_readToString":function() { + var str = {}; + var stringBits = []; + this.inputStream.QueryInterface(Components.interfaces.nsIUnicharInputStream); + while(1) { + var read = this.inputStream.readString(32768, str); + if(!read) break; + stringBits.push(str.value); + } + return stringBits.join(""); + }, + + "_initRDF":function() { + // call Zotero.wait() to do UI repaints + Zotero.wait(); + + // get URI + var IOService = Components.classes['@mozilla.org/network/io-service;1'] + .getService(Components.interfaces.nsIIOService); + var fileHandler = IOService.getProtocolHandler("file") + .QueryInterface(Components.interfaces.nsIFileProtocolHandler); + var baseURI = fileHandler.getURLSpecFromFile(this.file); + + Zotero.debug("Translate: Initializing RDF data store"); + this._dataStore = new Zotero.RDF.AJAW.RDFIndexedFormula(); + var parser = new Zotero.RDF.AJAW.RDFParser(this._dataStore); + try { + var nodes = Zotero.Translate.IO.parseDOMXML(this._rawStream, this._charset, this.file.fileSize); + parser.parse(nodes, baseURI); + + this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore); + } catch(e) { + this.close(); + throw "Translate: No RDF found"; + } + }, + + "setCharacterSet":function(charset) { + if(typeof charset !== "string") { + throw "Translate: setCharacterSet: charset must be a string"; + } + + // seek back to the beginning + this._seekToStart(this._allowCharsetOverride ? this._allowCharsetOverride : this._charset); + + if(!_allowCharsetOverride) { + Zotero.debug("Translate: setCharacterSet: translate charset override ignored due to BOM or XML parse instruction"); + } + }, + + "read":function(bytes) { + var str = {}; + + if(bytes) { + // read number of bytes requested + this.inputStream.QueryInterface(Components.interfaces.nsIUnicharInputStream); + var amountRead = this.inputStream.readString(bytes, str); + if(!amountRead) return false; + this.bytesRead += amountRead; + } else { + // bytes not specified; read a line + this.inputStream.QueryInterface(Components.interfaces.nsIUnicharLineInputStream); + if(this._linesExhausted) return false; + this._linesExhausted = !this.inputStream.readLine(str); + this.bytesRead += str.value.length+1; // only approximate + } + + return str.value; + }, + + "_getXML":function() { + if(this._mode == "xml/dom") { + return Zotero.Translate.IO.parseDOMXML(this._rawStream, this._charset, this.file.fileSize); + } else { + return this._readToString().replace(/<\?xml[^>]+\?>/, ""); + } + }, + + "reset":function(newMode) { + if(Zotero.Translate.IO.maintainedInstances.indexOf(this) === -1) { + Zotero.Translate.IO.maintainedInstances.push(this); + } + this._seekToStart(this._charset); + + this._mode = newMode; + if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1 && !this.RDF) { + this._initRDF(); + } + }, + + "close":function() { + var myIndex = Zotero.Translate.IO.maintainedInstances.indexOf(this); + if(myIndex !== -1) Zotero.Translate.IO.maintainedInstances.splice(myIndex, 1); + + if(this._rawStream) { + this._rawStream.close(); + delete this._rawStream; + } + } +} +Zotero.Translate.IO.Read.prototype.__defineGetter__("contentLength", +function() { + return this.file.fileSize; +}); + +/******* Write support *******/ + +Zotero.Translate.IO.Write = function(file, mode, charset) { + Zotero.Translate.IO.maintainedInstances.push(this); + this._rawStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + this._rawStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate + this._writtenToStream = false; + if(mode || charset) this.reset(mode, charset); +} + +Zotero.Translate.IO.Write.prototype = { + "__exposedProps__":{ + "RDF":"r", + "write":"r", + "setCharacterSet":"r" + }, + + "_initRDF":function() { + Zotero.debug("Translate: Initializing RDF data store"); + this._dataStore = new Zotero.RDF.AJAW.RDFIndexedFormula(); + this.RDF = new Zotero.Translate.IO._RDFSandbox(this._dataStore); + }, + + "setCharacterSet":function(charset) { + if(typeof charset !== "string") { + throw "Translate: setCharacterSet: charset must be a string"; + } + + if(!this.outputStream) { + this.outputStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Components.interfaces.nsIConverterOutputStream); + } + + if(charset == "UTF-8xBOM") charset = "UTF-8"; + this.outputStream.init(this._rawStream, charset, 1024, "?".charCodeAt(0)); + this._charset = charset; + }, + + "write":function(data) { + if(!this._charset) this.setCharacterSet("UTF-8"); + + if(!this._writtenToStream && this._charset.substr(this._charset.length-4) == "xBOM" + && BOMs[this._charset.substr(0, this._charset.length-4).toUpperCase()]) { + // If stream has not yet been written to, and a UTF type has been selected, write BOM + this._rawStream.write(BOMs[streamCharset], BOMs[streamCharset].length); + } + + if(this._charset == "MACINTOSH") { + // fix buggy Mozilla MacRoman + var splitData = data.split(/([\r\n]+)/); + for(var i=0; i<splitData.length; i+=2) { + // write raw newlines straight to the string + this.outputStream.writeString(splitData[i]); + if(splitData[i+1]) { + this._rawStream.write(splitData[i+1], splitData[i+1].length); + } + } + } else { + this.outputStream.writeString(data); + } + + this._writtenToStream = true; + }, + + "reset":function(newMode, charset) { + this._mode = newMode; + if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) { + this._initRDF(); + if(!this._writtenToString) this.setCharacterSet("UTF-8"); + } else if(!this._writtenToString) { + this.setCharacterSet(charset ? charset : "UTF-8"); + } + }, + + "close":function() { + if(Zotero.Translate.IO.rdfDataModes.indexOf(this._mode) !== -1) { + this.write(this.RDF.serialize()); + } + + var myIndex = Zotero.Translate.IO.maintainedInstances.indexOf(this); + if(myIndex !== -1) Zotero.Translate.IO.maintainedInstances.splice(myIndex, 1); + + this._rawStream.close(); + } +} diff --git a/chrome/content/zotero/xpcom/translation/item_local.js b/chrome/content/zotero/xpcom/translation/translate_item.js diff --git a/chrome/content/zotero/xpcom/translation/translator.js b/chrome/content/zotero/xpcom/translation/translator.js @@ -149,18 +149,153 @@ Zotero.Translators = new function() { /** * 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) { + this.get = function(id, callback) { if(!_initialized) this.init(); - return _translators[id] ? _translators[id] : false; + var translator = _translators[id] ? _translators[id] : false; + + if(callback) { + callback(translator); + return true; + } + return 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) this.init() + + var translators = _cache[type].slice(0); + if(callback) { + callback(translators); + return true; + } + return translators; } /** * Gets all translators for a specific type of translation */ - this.getAllForType = function(type) { + this.getAll = function() { if(!_initialized) this.init(); - return _cache[type].slice(0); + return [translator for each(translator in _translators)]; + } + + /** + * 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) { + var allTranslators = this.getAllForType("web"); + var potentialTranslators = []; + + var properHosts = []; + var proxyHosts = []; + + var properURI = Zotero.Proxies.proxyToProper(uri); + var knownProxy = properURI !== uri; + if(knownProxy) { + // if we know this proxy, just use the proper URI for detection + var searchURIs = [properURI]; + } else { + var searchURIs = [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); + if(m) { + var hostnames = m[2].split("."); + for(var i=1; i<hostnames.length-2; i++) { + if(TLDS[hostnames[i].toLowerCase()]) { + var properHost = hostnames.slice(0, i+1).join("."); + searchURIs.push(m[1]+properHost+uri.substr(m[0].length)); + properHosts.push(properHost); + proxyHosts.push(hostnames.slice(i+1).join(".")); + } + } + } + } + + Zotero.debug("Translators: Looking for translators for "+searchURIs.join(", ")); + + var converterFunctions = []; + for(var i=0; i<allTranslators.length; i++) { + for(var j=0; j<searchURIs.length; j++) { + if((!allTranslators[i].webRegexp + && allTranslators[i].runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) + || (uri.length < 8192 && allTranslators[i].webRegexp.test(searchURIs[j]))) { + // add translator to list + potentialTranslators.push(allTranslators[i]); + + if(j === 0) { + if(knownProxy) { + converterFunctions.push(Zotero.Proxies.properToProxy); + } else { + converterFunctions.push(null); + } + } else { + converterFunctions.push(new function() { + var re = new RegExp('^https?://(?:[^/]\\.)?'+Zotero.Utilities.quotemeta(properHosts[j-1]), "gi"); + var proxyHost = proxyHosts[j-1].replace(/\$/g, "$$$$"); + return function(uri) { return uri.replace(re, "$&."+proxyHost) }; + }); + } + + // don't add translator more than once + break; + } + } + } + + if(callback) { + callback([potentialTranslators, converterFunctions]); + return true; + } + return potentialTranslators; + } + + /** + * Gets import translators for a specific location + * @param {String} location The location 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. + */ + this.getImportTranslatorsForLocation = function(location, callback) { + var allTranslators = Zotero.Translators.getAllForType("import"); + var tier1Translators = []; + var tier2Translators = []; + + for(var i=0; i<allTranslators.length; i++) { + if(allTranslators[i].importRegexp.test(location)) { + tier1Translators.push(allTranslators[i]); + } else { + tier2Translators.push(allTranslators[i]); + } + } + + var translators = tier1Translators.concat(tier2Translators); + if(callback) { + callback(translators); + return true; + } + return translators; } /** @@ -171,7 +306,6 @@ Zotero.Translators = new function() { return Zotero.File.getValidFileName(label) + ".js"; } - /** * @param {String} metadata * @param {String} metadata.translatorID Translator GUID @@ -262,29 +396,6 @@ Zotero.Translators = new function() { } } -/** - * @class Represents an individual translator - * @constructor - * @param {nsIFile} file File from which to generate a translator object - * @property {String} translatorID Unique GUID of the translator - * @property {Integer} translatorType Type of the translator (use bitwise & with TRANSLATOR_TYPES to read) - * @property {String} label Human-readable name of the translator - * @property {String} creator Author(s) of the translator - * @property {String} target Location that the translator processes - * @property {String} minVersion Minimum Zotero version - * @property {String} maxVersion Minimum Zotero version - * @property {Integer} priority Lower-priority translators will be selected first - * @property {String} browserSupport String indicating browser supported by the translator - * g = Gecko (Firefox) - * c = Google Chrome (WebKit & V8) - * s = Safari (WebKit & Nitro/Squirrelfish Extreme) - * i = Internet Explorer - * @property {Object} configOptions Configuration options for import/export - * @property {Object} displayOptions Display options for export - * @property {Boolean} inRepository Whether the translator may be found in the repository - * @property {String} lastUpdated SQL-style date and time of translator's last update - * @property {String} code The executable JavaScript for the translator - */ Zotero.Translator = function(file, json, code) { const codeGetterFunction = function() { return Zotero.File.getContents(this.file); } // Maximum length for the info JSON in a translator @@ -355,6 +466,7 @@ Zotero.Translator = function(file, json, code) { this._configOptions = info["configOptions"] ? info["configOptions"] : {}; this._displayOptions = info["displayOptions"] ? info["displayOptions"] : {}; this.browserSupport = info["browserSupport"] ? info["browserSupport"] : "g"; + this.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER; if(this.translatorType & TRANSLATOR_TYPES["import"]) { // compile import regexp to match only file extension @@ -374,7 +486,6 @@ Zotero.Translator = function(file, json, code) { try { this.webRegexp = this.target ? new RegExp(this.target, "i") : null; } catch(e) { - if(fStream) fStream.close(); this.logError("Invalid target in " + file.leafName); this.webRegexp = null; if(fStream) fStream.close(); @@ -420,4 +531,8 @@ Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, var ios = Components.classes["@mozilla.org/network/io-service;1"]. getService(Components.interfaces.nsIIOService); Zotero.log(message, type ? type : "error", ios.newFileURI(this.file).spec); -} -\ No newline at end of file +} + +Zotero.Translator.RUN_MODE_IN_BROWSER = 1; +Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE = 2; +Zotero.Translator.RUN_MODE_ZOTERO_SERVER = 4; +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -595,6 +595,17 @@ Zotero.Utilities = { } catch(e) { return false; } + }, + + /** + * Escapes metacharacters in a literal so that it may be used in a regular expression + */ + "quotemeta":function(literal) { + if(typeof literal !== "string") { + throw "Argument "+literal+" must be a string in Zotero.Utilities.quotemeta()"; + } + const metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g; + return literal.replace(metaRegexp, "\\$&"); } } @@ -746,13 +757,11 @@ Zotero.Utilities.Translate.prototype.loadDocument = function(url, succeeded, fai * @ignore */ Zotero.Utilities.Translate.prototype.processDocuments = function(urls, processor, done, exception) { - if(this._translate.locationIsProxied) { - if(typeof(urls) == "string") { - urls = [this._convertURL(urls)]; - } else { - for(var i in urls) { - urls[i] = this._convertURL(urls[i]); - } + if(typeof(urls) == "string") { + urls = [this._convertURL(urls)]; + } else { + for(var i in urls) { + urls[i] = this._convertURL(urls[i]); } } @@ -776,7 +785,7 @@ Zotero.Utilities.Translate.prototype.processDocuments = function(urls, processor * @return {Document} DOM document object */ Zotero.Utilities.Translate.prototype.retrieveDocument = function(url) { - if(this._translate.locationIsProxied) url = this._convertURL(url); + url = this._convertURL(url); var mainThread = Zotero.mainThread; var loaded = false; @@ -822,7 +831,7 @@ Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, header /* Apparently, a synchronous XMLHttpRequest would have the behavior of this routine in FF3, but * in FF3.5, synchronous XHR blocks all JavaScript on the thread. See * http://hacks.mozilla.org/2009/07/synchronous-xhr/. */ - if(this._translate.locationIsProxied) url = this._convertURL(url); + url = this._convertURL(url); if(!headers) headers = null; if(!responseCharset) responseCharset = null; @@ -911,15 +920,25 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, header */ Zotero.Utilities.Translate.prototype._convertURL = function(url) { const protocolRe = /^(?:(?:http|https|ftp):)/i; - const fileRe = /^[^:]*/; - if(this._translate.locationIsProxied) { - url = Zotero.Proxies.properToProxy(url); + // convert proxy to proper if applicable + if(this._translate.translator && this._translate.translator[0] + && this._translate.translator[0].properToProxy) { + url = this._translate.translator[0].properToProxy(url); } - if(protocolRe.test(url)) return url; - if(!fileRe.test(url)) { - throw "Invalid URL supplied for HTTP request"; + + if(Zotero.isChrome || Zotero.isSafari) { + // this code is sandboxed, so we don't worry + return url; } else { + if(protocolRe.test(url)) return url; + + if(uri.indexOf(":") !== -1) { + // don't allow protocol switches + throw "Invalid URL supplied for HTTP request"; + } + + // resolve relative URIs return Components.classes["@mozilla.org/network/io-service;1"]. getService(Components.interfaces.nsIIOService). newURI(this._translate.location, "", null).resolve(url); diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -36,6 +36,8 @@ const ZOTERO_CONFIG = { PREF_BRANCH: 'extensions.zotero.' }; +const ZOTERO_METAREGEXP = /[-[\]{}()*+?.\\^$|,#\s]/g; + // Fx4.0b8+ use implicit SJOWs and get rid of explicit XPCSafeJSObjectWrapper constructor // Ugly hack to get around this until we can just kill the XPCSafeJSObjectWrapper calls (when we // drop Fx3.6 support) @@ -45,10 +47,17 @@ try { eval("var XPCSafeJSObjectWrapper = function(arg) { return arg }"); } +// Load AddonManager for Firefox 4 +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"); +} + /* * 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 @@ -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 @@ -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-command-line-handler.js b/components/zotero-command-line-handler.js @@ -0,0 +1,120 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + + Based on nsChromeExtensionHandler example code by Ed Anuff at + http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol + + ***** END LICENSE BLOCK ***** +*/ + +/* + Based on nsICommandLineHandler example code at + https://developer.mozilla.org/en/Chrome/Command_Line +*/ + +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"; +const clh_description = "Zotero Command Line Handler"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +/** + * The XPCOM component that implements nsICommandLineHandler. + */ +function ZoteroCommandLineHandler() {} +ZoteroCommandLineHandler.prototype = { + /* nsISupports */ + 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); + 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, + classID: clh_CID, + contractID: clh_contractID, + service: true, + _xpcom_categories: [{category:"command-line-handler", entry:clh_category}], + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler, + 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([ZoteroCommandLineHandler]); +} else { + var NSGetModule = XPCOMUtils.generateNSGetModule([ZoteroCommandLineHandler]); +} +\ No newline at end of file diff --git a/components/zotero-integration-service.js b/components/zotero-integration-service.js @@ -1,99 +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 <http://www.gnu.org/licenses/>. - - - Based on nsChromeExtensionHandler example code by Ed Anuff at - http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol - - ***** END LICENSE BLOCK ***** -*/ - -/* - Based on nsICommandLineHandler example code at - 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_CID = Components.ID("{531828f8-a16c-46be-b9aa-14845c3b010f}"); -const clh_category = "m-zotero-integration"; -const clh_description = "Zotero Integration Command Line Handler"; - -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); - -/** - * The XPCOM component that implements nsICommandLineHandler. - */ -function ZoteroIntegrationCommandLineHandler() {} -ZoteroIntegrationCommandLineHandler.prototype = { - Zotero : null, - - /* nsISupports */ - QueryInterface : function(iid) { - if(iid.equals(nsICommandLineHandler) || - iid.equals(nsIFactory) || - iid.equals(nsISupports)) return this; - throw Components.results.NS_ERROR_NO_INTERFACE; - }, - - /* nsICommandLineHandler */ - handle : function(cmdLine) { - var agent = cmdLine.handleFlagWithParam("ZoteroIntegrationAgent", false); - var command = cmdLine.handleFlagWithParam("ZoteroIntegrationCommand", false); - 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; - // Not quite sure why this is necessary to get the appropriate scoping - var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); - timer.initWithCallback({notify:function() { Zotero.Integration.execCommand(agent, command, docId) }}, 0, - Components.interfaces.nsITimer.TYPE_ONE_SHOT); - } - }, - - classDescription: clh_description, - classID: clh_CID, - contractID: clh_contractID, - service: true, - _xpcom_categories: [{category:"command-line-handler", entry:clh_category}], - QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsICommandLineHandler, - Components.interfaces.nsISupports]) -}; - -/** -* 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]); -} else { - var NSGetModule = XPCOMUtils.generateNSGetModule([ZoteroIntegrationCommandLineHandler]); -} -\ No newline at end of file diff --git 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 @@ -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"); +/** XPCOM files to be loaded only for connector translation and DB access **/ +const xpcomFilesConnector = [ + 'connector/translate_item', + 'connector/translator', + 'connector/connector', + 'connector/cachedTypes' +]; -// 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); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -for (var i=1; i<xpcomFiles.length; i++) { - try { +var instanceID = (new Date()).getTime(); +var isFirstLoadThisSession = true; +var zContext = null; + +ZoteroContext = function() {} +ZoteroContext.prototype = { + /** + * Convenience method to replicate window.alert() + **/ + // TODO: is this still used? if so, move to zotero.js + "alert":function alert(msg){ + this.Zotero.debug("alert() is deprecated from Zotero XPCOM"); + Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService) + .alert(null, "", msg); + }, + + /** + * Convenience method to replicate window.confirm() + **/ + // TODO: is this still used? if so, move to zotero.js + "confirm":function confirm(msg){ + this.Zotero.debug("confirm() is deprecated from Zotero XPCOM"); + return Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService) + .confirm(null, "", msg); + }, + + "Cc":Cc, + "Ci":Ci, + + /** + * Convenience method to replicate window.setTimeout() + **/ + "setTimeout":function setTimeout(func, ms){ + this.Zotero.setTimeout(func, ms); + }, + + /** + * Switches in or out of connector mode + */ + "switchConnectorMode":function(isConnector) { + if(isConnector !== this.isConnector) { + zContext.Zotero.shutdown(); + + // create a new zContext + makeZoteroContext(isConnector); + zContext.Zotero.init(); + } + + return zContext; + } +}; + +/** + * The class from which the Zotero global XPCOM context is constructed + * + * @constructor + * This runs when ZoteroService is first requested to load all applicable scripts and initialize + * Zotero. Calls to other XPCOM components must be in here rather than in top-level code, as other + * components may not have yet been initialized. + */ +function makeZoteroContext(isConnector) { + if(zContext) { + // Swap out old zContext + var oldzContext = zContext; + // Create new zContext + zContext = new ZoteroContext(); + // Swap in old Zotero object, so that references don't break, but empty it + zContext.Zotero = oldzContext.Zotero; + for(var key in zContext.Zotero) delete zContext.Zotero[key]; + } else { + zContext = new ZoteroContext(); + zContext.Zotero = function() {}; + } + + // Load zotero.js first + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFilesAll[0] + ".js", zContext); + + // Load CiteProc into Zotero.CiteProc namespace + zContext.Zotero.CiteProc = {"Zotero":zContext.Zotero}; + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://zotero/content/xpcom/citeproc.js", zContext.Zotero.CiteProc); + + // Load remaining xpcomFiles + for (var i=1; i<xpcomFilesAll.length; i++) { + try { + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFilesAll[i] + ".js", zContext); + } + catch (e) { + Components.utils.reportError("Error loading " + xpcomFilesAll[i] + ".js", zContext); + throw (e); + } + } + + // Load xpcomFiles for specific mode + for each(var xpcomFile in (isConnector ? xpcomFilesConnector : xpcomFilesLocal)) { + try { + Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFile + ".js", zContext); + } + catch (e) { + Components.utils.reportError("Error loading " + xpcomFile + ".js", zContext); + throw (e); + } + } + + // Load RDF files into Zotero.RDF.AJAW namespace (easier than modifying all of the references) + const rdfXpcomFiles = [ + 'rdf/uri', + 'rdf/term', + 'rdf/identity', + 'rdf/match', + 'rdf/n3parser', + 'rdf/rdfparser', + 'rdf/serialize', + 'rdf' + ]; + zContext.Zotero.RDF = {AJAW:{Zotero:zContext.Zotero}}; + for (var i=0; i<rdfXpcomFiles.length; i++) { Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/" + xpcomFiles[i] + ".js"); - } - catch (e) { - Components.utils.reportError("Error loading " + xpcomFiles[i] + ".js"); - throw (e); + .loadSubScript("chrome://zotero/content/xpcom/" + rdfXpcomFiles[i] + ".js", zContext.Zotero.RDF.AJAW); } -} - - -// Load RDF files into Zotero.RDF.AJAW namespace (easier than modifying all of the references) -var rdfXpcomFiles = [ - 'rdf/uri', - 'rdf/term', - 'rdf/identity', - 'rdf/match', - 'rdf/n3parser', - 'rdf/rdfparser', - 'rdf/serialize', - 'rdf' -]; - -Zotero.RDF = {AJAW:{}}; - -for (var i=0; i<rdfXpcomFiles.length; i++) { + + // load nsTransferable (query: do we still use this?) Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://zotero/content/xpcom/" + rdfXpcomFiles[i] + ".js", Zotero.RDF.AJAW); -} - -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://global/content/nsTransferable.js"); - -/********************************************************************/ - + .loadSubScript("chrome://global/content/nsTransferable.js", zContext); + + // add connector-related properties + zContext.Zotero.isConnector = isConnector; + zContext.Zotero.instanceID = instanceID; + zContext.Zotero.__defineGetter__("isFirstLoadThisSession", function() isFirstLoadThisSession); +}; -// Initialize the Zotero service -// -// This runs when ZoteroService is first requested. -// Calls to other XPCOM components must be in here rather than in top-level -// code, as other components may not have yet been initialized. -function setupService(){ +/** + * The class representing the Zotero service, and affiliated XPCOM goop + */ +function ZoteroService(){ try { - Zotero.init(); - } - catch (e) { + if(isFirstLoadThisSession) { + makeZoteroContext(false); + try { + zContext.Zotero.init(); + } catch(e) { + if(e === "ZOTERO_SHOULD_START_AS_CONNECTOR") { + // if Zotero should start as a connector, reload it + zContext.Zotero.shutdown(); + makeZoteroContext(true); + zContext.Zotero.init(); + } else { + dump(e.toSource()); + Components.utils.reportError(e); + throw e; + } + } + } + isFirstLoadThisSession = false; // no longer first load + this.wrappedJSObject = zContext.Zotero; + } catch(e) { var msg = typeof e == 'string' ? e : e.name; dump(e + "\n\n"); Components.utils.reportError(e); - throw (e); + throw e; } } -function ZoteroService(){ - this.wrappedJSObject = ZoteroWrapped.Zotero; - setupService(); -} - - -/** -* Convenience method to replicate window.alert() -**/ -// TODO: is this still used? if so, move to zotero.js -function alert(msg){ - Cc["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Ci.nsIPromptService) - .alert(null, "", msg); -} - -/** -* Convenience method to replicate window.confirm() -**/ -// TODO: is this still used? if so, move to zotero.js -function confirm(msg){ - return Cc["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Ci.nsIPromptService) - .confirm(null, "", msg); -} - - -/** -* Convenience method to replicate window.setTimeout() -**/ -function setTimeout(func, ms) { - Zotero.setTimeout(func, ms); -} - - // // XPCOM goop // @@ -227,8 +301,8 @@ ZoteroService.prototype = { contractID: ZOTERO_CONTRACTID, classDescription: ZOTERO_CLASSNAME, classID: ZOTERO_CID, - service: true, - QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, ZOTERO_IID]) + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports, + Components.interfaces.nsIProtocolHandler]) } /** diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js @@ -107,8 +107,8 @@ pref("extensions.zotero.integration.port", 50001); pref("extensions.zotero.integration.autoRegenerate", -1); // -1 = ask; 0 = no; 1 = yes // Connector settings -pref("extensions.zotero.connector.enabled", false); -pref("extensions.zotero.connector.port", 23119); // ascii "ZO" +pref("extensions.zotero.httpServer.enabled", false); // TODO enabled for testing only +pref("extensions.zotero.httpServer.port", 23119); // ascii "ZO" // Zeroconf pref("extensions.zotero.zeroconf.server.enabled", false);