commit 0490886f28033c604980f0b41d82549e1155610d parent 4384a8ecc28948158a2ba1bd9d5066a488cc503b Author: Simon Kornblith <simon@simonster.com> Date: Sat, 17 Aug 2013 03:47:20 -0400 Merge branch 'async-translators' Diffstat:
17 files changed, 933 insertions(+), 914 deletions(-)
diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js @@ -41,55 +41,56 @@ var Zotero_File_Exporter = function() { * Performs the actual export operation **/ Zotero_File_Exporter.prototype.save = function() { - var translation = new Zotero.Translate.Export(); - var translators = translation.getTranslators(); - - // present options dialog - var io = {translators:translators} - window.openDialog("chrome://zotero/content/exportOptions.xul", - "_blank", "chrome,modal,centerscreen,resizable=no", io); - if(!io.selectedTranslator) { - return false; - } - - const nsIFilePicker = Components.interfaces.nsIFilePicker; - var fp = Components.classes["@mozilla.org/filepicker;1"] - .createInstance(nsIFilePicker); - fp.init(window, Zotero.getString("fileInterface.export"), nsIFilePicker.modeSave); - - // set file name and extension - if(io.displayOptions.exportFileData) { - // if the result will be a folder, don't append any extension or use - // filters - fp.defaultString = this.name; - fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll); - } else { - // if the result will be a file, append an extension and use filters - fp.defaultString = this.name+(io.selectedTranslator.target ? "."+io.selectedTranslator.target : ""); - fp.defaultExtension = io.selectedTranslator.target; - fp.appendFilter(io.selectedTranslator.label, "*."+(io.selectedTranslator.target ? io.selectedTranslator.target : "*")); - } - - var rv = fp.show(); - if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) { - if(this.collection) { - translation.setCollection(this.collection); - } else if(this.items) { - translation.setItems(this.items); + var translation = new Zotero.Translate.Export(), + me = this; + translation.getTranslators().then(function(translators) { + // present options dialog + var io = {translators:translators} + window.openDialog("chrome://zotero/content/exportOptions.xul", + "_blank", "chrome,modal,centerscreen,resizable=no", io); + if(!io.selectedTranslator) { + return false; } - translation.setLocation(fp.file); - translation.setTranslator(io.selectedTranslator); - translation.setDisplayOptions(io.displayOptions); - translation.setHandler("itemDone", Zotero_File_Interface.updateProgress); - translation.setHandler("done", this._exportDone); - Zotero.UnresponsiveScriptIndicator.disable(); - Zotero_File_Interface.Progress.show( - Zotero.getString("fileInterface.itemsExported") - ); - translation.translate() - } - return false; + const nsIFilePicker = Components.interfaces.nsIFilePicker; + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + fp.init(window, Zotero.getString("fileInterface.export"), nsIFilePicker.modeSave); + + // set file name and extension + if(io.displayOptions.exportFileData) { + // if the result will be a folder, don't append any extension or use + // filters + fp.defaultString = me.name; + fp.appendFilters(Components.interfaces.nsIFilePicker.filterAll); + } else { + // if the result will be a file, append an extension and use filters + fp.defaultString = me.name+(io.selectedTranslator.target ? "."+io.selectedTranslator.target : ""); + fp.defaultExtension = io.selectedTranslator.target; + fp.appendFilter(io.selectedTranslator.label, "*."+(io.selectedTranslator.target ? io.selectedTranslator.target : "*")); + } + + var rv = fp.show(); + if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) { + if(me.collection) { + translation.setCollection(me.collection); + } else if(me.items) { + translation.setItems(me.items); + } + + translation.setLocation(fp.file); + translation.setTranslator(io.selectedTranslator); + translation.setDisplayOptions(io.displayOptions); + translation.setHandler("itemDone", Zotero_File_Interface.updateProgress); + translation.setHandler("done", me._exportDone); + Zotero.UnresponsiveScriptIndicator.disable(); + Zotero_File_Interface.Progress.show( + Zotero.getString("fileInterface.itemsExported") + ); + translation.translate() + } + return false; + }).done(); } /* @@ -207,9 +208,7 @@ var Zotero_File_Interface = new function() { } var translation = new Zotero.Translate.Import(); - if(!file) { - var translators = translation.getTranslators(); - + (file ? Q(file) : translation.getTranslators().then(function(translators) { const nsIFilePicker = Components.interfaces.nsIFilePicker; var fp = Components.classes["@mozilla.org/filepicker;1"] .createInstance(nsIFilePicker); @@ -225,15 +224,17 @@ var Zotero_File_Interface = new function() { return false; } - file = fp.file; - } - - translation.setLocation(file); - // get translators again, bc now we can check against the file - translation.setHandler("translators", function(obj, item) { - _importTranslatorsAvailable(obj, item, createNewCollection); - }); - translators = translation.getTranslators(); + return fp.file; + })).then(function(file) { + if(!file) return; // no file if user cancelled + + translation.setLocation(file); + // get translators again, bc now we can check against the file + translation.setHandler("translators", function(obj, item) { + _importTranslatorsAvailable(obj, item, createNewCollection); + }); + translators = translation.getTranslators(); + }).done(); } diff --git a/chrome/content/zotero/lookup.js b/chrome/content/zotero/lookup.js @@ -115,31 +115,32 @@ const Zotero_Lookup = new function () { translate.setSearch(item); // be lenient about translators - var translators = translate.getTranslators(); - translate.setTranslator(translators); - - translate.setHandler("done", function(translate, success) { - notDone--; - successful += success; - - if(!notDone) { //i.e. done - Zotero_Lookup.toggleProgress(false); - if(successful) { - document.getElementById("zotero-lookup-panel").hidePopup(); - } else { - var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - prompts.alert(window, Zotero.getString("lookup.failure.title"), - Zotero.getString("lookup.failure.description")); + translate.getTranslators().then(function(translators) { + translate.setTranslator(translators); + + translate.setHandler("done", function(translate, success) { + notDone--; + successful += success; + + if(!notDone) { //i.e. done + Zotero_Lookup.toggleProgress(false); + if(successful) { + document.getElementById("zotero-lookup-panel").hidePopup(); + } else { + var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + prompts.alert(window, Zotero.getString("lookup.failure.title"), + Zotero.getString("lookup.failure.description")); + } } - } - }); + }); - translate.setHandler("itemDone", function(obj, item) { - if(collection) collection.addItem(item.id); + translate.setHandler("itemDone", function(obj, item) { + if(collection) collection.addItem(item.id); + }); + + translate.translate(libraryID); }); - - translate.translate(libraryID); })(item); } diff --git a/chrome/content/zotero/preferences/preferences_export.js b/chrome/content/zotero/preferences/preferences_export.js @@ -45,8 +45,8 @@ Zotero_Preferences.Export = { // Initialize default format drop-down var format = Zotero.Prefs.get("export.quickCopy.setting"); var menulist = document.getElementById("zotero-quickCopy-menu"); - this.buildQuickCopyFormatDropDown(menulist, Zotero.QuickCopy.getContentType(format), format); menulist.setAttribute('preference', "pref-quickCopy-setting"); + this.buildQuickCopyFormatDropDown(menulist, Zotero.QuickCopy.getContentType(format), format); this.updateQuickCopyHTMLCheckbox(); if (!Zotero.isStandalone) { @@ -106,30 +106,30 @@ Zotero_Preferences.Export = { // add export formats to list var translation = new Zotero.Translate("export"); - var translators = translation.getTranslators(); - - for (var i=0; i<translators.length; i++) { - // Skip RDF formats - switch (translators[i].translatorID) { - case '6e372642-ed9d-4934-b5d1-c11ac758ebb7': - case '14763d24-8ba0-45df-8f52-b8d1108e7ac9': - continue; + translation.getTranslators() + .then(function (translators) { + for (var i=0; i<translators.length; i++) { + // Skip RDF formats + switch (translators[i].translatorID) { + case '6e372642-ed9d-4934-b5d1-c11ac758ebb7': + case '14763d24-8ba0-45df-8f52-b8d1108e7ac9': + continue; + } + var val = 'export=' + translators[i].translatorID; + var itemNode = document.createElement("menuitem"); + itemNode.setAttribute("value", val); + itemNode.setAttribute("label", translators[i].label); + itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyHTMLCheckbox()'); + popup.appendChild(itemNode); + + if (val == currentFormat) { + menulist.selectedItem = itemNode; + } } - var val = 'export=' + translators[i].translatorID; - var itemNode = document.createElement("menuitem"); - itemNode.setAttribute("value", val); - itemNode.setAttribute("label", translators[i].label); - itemNode.setAttribute("oncommand", 'Zotero_Preferences.Export.updateQuickCopyHTMLCheckbox()'); - popup.appendChild(itemNode); - if (val == currentFormat) { - menulist.selectedItem = itemNode; - } - } - - menulist.click(); - - return popup; + menulist.click(); + }) + .done(); }, @@ -156,25 +156,28 @@ Zotero_Preferences.Export = { var asHTML = treerow.childNodes[2].getAttribute('label') != ''; } - var format = Zotero.QuickCopy.getSettingFromFormattedName(format); - if (asHTML) { - format = format.replace('bibliography=', 'bibliography/html='); - } - - var io = {domain: domain, format: format, ok: false}; - window.openDialog('chrome://zotero/content/preferences/quickCopySiteEditor.xul', "zotero-preferences-quickCopySiteEditor", "chrome, modal", io); - - if (!io.ok) { - return; - } - - if (domain && domain != io.domain) { - Zotero.DB.query("DELETE FROM settings WHERE setting='quickCopySite' AND key=?", [domain]); - } - - Zotero.DB.query("REPLACE INTO settings VALUES ('quickCopySite', ?, ?)", [io.domain, io.format]); - - this.refreshQuickCopySiteList(); + Zotero.QuickCopy.getSettingFromFormattedName(format) + .then(function (format) { + if (asHTML) { + format = format.replace('bibliography=', 'bibliography/html='); + } + + var io = {domain: domain, format: format, ok: false}; + window.openDialog('chrome://zotero/content/preferences/quickCopySiteEditor.xul', "zotero-preferences-quickCopySiteEditor", "chrome, modal", io); + + if (!io.ok) { + return; + } + + if (domain && domain != io.domain) { + Zotero.DB.query("DELETE FROM settings WHERE setting='quickCopySite' AND key=?", [domain]); + } + + Zotero.DB.query("REPLACE INTO settings VALUES ('quickCopySite', ?, ?)", [io.domain, io.format]); + + this.refreshQuickCopySiteList(); + }.bind(this)) + .done(); }, @@ -192,26 +195,30 @@ Zotero_Preferences.Export = { return; } - for (var i=0; i<siteData.length; i++) { - var treeitem = document.createElement('treeitem'); - var treerow = document.createElement('treerow'); - var domainCell = document.createElement('treecell'); - var formatCell = document.createElement('treecell'); - var HTMLCell = document.createElement('treecell'); - - domainCell.setAttribute('label', siteData[i].domainPath); - - var formatted = Zotero.QuickCopy.getFormattedNameFromSetting(siteData[i].format); - formatCell.setAttribute('label', formatted); - var copyAsHTML = Zotero.QuickCopy.getContentType(siteData[i].format) == 'html'; - HTMLCell.setAttribute('label', copyAsHTML ? ' ✓ ' : ''); - - treerow.appendChild(domainCell); - treerow.appendChild(formatCell); - treerow.appendChild(HTMLCell); - treeitem.appendChild(treerow); - treechildren.appendChild(treeitem); - } + Q.async(function () { + for (var i=0; i<siteData.length; i++) { + let treeitem = document.createElement('treeitem'); + let treerow = document.createElement('treerow'); + let domainCell = document.createElement('treecell'); + let formatCell = document.createElement('treecell'); + let HTMLCell = document.createElement('treecell'); + + domainCell.setAttribute('label', siteData[i].domainPath); + + yield Zotero.QuickCopy.getFormattedNameFromSetting(siteData[i].format) + .then(function (formatted) { + formatCell.setAttribute('label', formatted); + var copyAsHTML = Zotero.QuickCopy.getContentType(siteData[i].format) == 'html'; + HTMLCell.setAttribute('label', copyAsHTML ? ' ✓ ' : ''); + + treerow.appendChild(domainCell); + treerow.appendChild(formatCell); + treerow.appendChild(HTMLCell); + treeitem.appendChild(treerow); + treechildren.appendChild(treeitem); + }); + } + })().done(); }, diff --git a/chrome/content/zotero/tools/testTranslators/testTranslators.js b/chrome/content/zotero/tools/testTranslators/testTranslators.js @@ -466,12 +466,13 @@ function init() { // get translators, with code for unsupported translators if(!viewerMode) { - Zotero.Translators.getAllForType(translatorType, new function() { + Zotero.Translators.getAllForType(translatorType, true). + then(new function() { var type = translatorType; return function(translators) { haveTranslators(translators, type); } - }, true); + }); } } diff --git a/chrome/content/zotero/tools/testTranslators/translatorTester.js b/chrome/content/zotero/tools/testTranslators/translatorTester.js @@ -48,7 +48,8 @@ Zotero_TranslatorTesters = new function() { var testers = []; var waitingForTranslators = TEST_TYPES.length; for(var i=0; i<TEST_TYPES.length; i++) { - Zotero.Translators.getAllForType(TEST_TYPES[i], new function() { + Zotero.Translators.getAllForType(TEST_TYPES[i], true). + then(new function() { var type = TEST_TYPES[i]; return function(translators) { try { @@ -66,7 +67,7 @@ Zotero_TranslatorTesters = new function() { Zotero.logError(e); } }; - }, true); + }); }; }; diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -2980,7 +2980,6 @@ Zotero.Item.prototype.fileExistsAsync = function () { } var nsIFile = self.getFile(null, true); - Components.utils.import("resource://gre/modules/osfile.jsm"); return Q(OS.File.exists(nsIFile.path)) .then(function(exists) { self._updateAttachmentStates(exists); diff --git a/chrome/content/zotero/xpcom/mimeTypeHandler.js b/chrome/content/zotero/xpcom/mimeTypeHandler.js @@ -145,15 +145,16 @@ Zotero.MIMETypeHandler = new function () { translation.setString(string); // attempt to retrieve translators - var translators = translation.getTranslators(); - if(!translators.length) { - // we lied. we can't really translate this file. - throw "No translator found for handled RIS, Refer or ISI file" - } - - // translate using first available - translation.setTranslator(translators[0]); - frontWindow.Zotero_Browser.performTranslation(translation); + return translation.getTranslators().then(function(translators) { + if(!translators.length) { + // we lied. we can't really translate this file. + throw "No translator found for handled RIS, Refer or ISI file" + } + + // translate using first available + translation.setTranslator(translators[0]); + frontWindow.Zotero_Browser.performTranslation(translation); + }); } /** @@ -268,7 +269,7 @@ Zotero.MIMETypeHandler = new function () { if (!this._storageStream) { this._storageStream = Components.classes["@mozilla.org/storagestream;1"]. createInstance(Components.interfaces.nsIStorageStream); - this._storageStream.init(4096, 4294967295, null); // PR_UINT32_MAX + this._storageStream.init(16384, 4294967295, null); // PR_UINT32_MAX this._outputStream = this._storageStream.getOutputStream(0); this._binaryInputStream = Components.classes["@mozilla.org/binaryinputstream;1"]. @@ -291,38 +292,36 @@ Zotero.MIMETypeHandler = new function () { const replacementChar = Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER; var convStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] .createInstance(Components.interfaces.nsIConverterInputStream); - convStream.init(inputStream, charset, 1024, replacementChar); + convStream.init(inputStream, charset, 16384, replacementChar); var readString = ""; var str = {}; - while (convStream.readString(4096, str) != 0) { + while (convStream.readString(16384, str) != 0) { readString += str.value; } convStream.close(); inputStream.close(); - try { - _typeHandlers[this._contentType](readString, (this._request.name ? this._request.name : null), - this._contentType, channel); - } catch(e) { - // if there was an error, handle using nsIExternalHelperAppService - var externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]. - getService(Components.interfaces.nsIExternalHelperAppService); - var frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. - getService(Components.interfaces.nsIWindowWatcher).activeWindow; - - var inputStream = this._storageStream.newInputStream(0); - var streamListener = externalHelperAppService.doContent(this._contentType, this._request, frontWindow, null); - if (streamListener) { - streamListener.onStartRequest(channel, context); - streamListener.onDataAvailable(this._request, context, inputStream, 0, this._storageStream.length); - streamListener.onStopRequest(channel, context, status); - } - this._storageStream.close(); - - // then throw our error - throw e; - } - - this._storageStream.close(); + var me = this; + Q(_typeHandlers[this._contentType](readString, (this._request.name ? this._request.name : null), + this._contentType, channel)).fail(function(e) { + // if there was an error, handle using nsIExternalHelperAppService + var externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Components.interfaces.nsIExternalHelperAppService); + var frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Components.interfaces.nsIWindowWatcher).activeWindow; + + var inputStream = me._storageStream.newInputStream(0); + var streamListener = externalHelperAppService.doContent(me._contentType, me._request, frontWindow, null); + if (streamListener) { + streamListener.onStartRequest(channel, context); + streamListener.onDataAvailable(me._request, context, inputStream, 0, me._storageStream.length); + streamListener.onStopRequest(channel, context, status); + } + + // then throw our error + throw e; + }).fin(function() { + me._storageStream.close(); + }).done(); } } diff --git a/chrome/content/zotero/xpcom/quickCopy.js b/chrome/content/zotero/xpcom/quickCopy.js @@ -25,38 +25,57 @@ Zotero.QuickCopy = new function() { - this.getFormattedNameFromSetting = getFormattedNameFromSetting; - this.getSettingFromFormattedName = getSettingFromFormattedName; this.getContentType = getContentType; this.stripContentType = stripContentType; this.getFormatFromURL = getFormatFromURL; this.getContentFromItems = getContentFromItems; - var _initialized = false; var _formattedNames = {}; - function getFormattedNameFromSetting(setting) { - if (!_initialized) { - _init(); - } + var _init = Zotero.lazy(function () { + Zotero.debug("Initializing Quick Copy"); - var name = _formattedNames[this.stripContentType(setting)]; - return name ? name : ''; + var translation = new Zotero.Translate.Export; + return translation.getTranslators() + .then(function (translators) { + // add styles to list + var styles = Zotero.Styles.getVisible(); + for each(var style in styles) { + _formattedNames['bibliography=' + style.styleID] = style.title; + } + + for (var i=0; i<translators.length; i++) { + // Skip RDF formats + switch (translators[i].translatorID) { + case '6e372642-ed9d-4934-b5d1-c11ac758ebb7': + case '14763d24-8ba0-45df-8f52-b8d1108e7ac9': + continue; + } + _formattedNames['export=' + translators[i].translatorID] = translators[i].label; + } + }); + }); + + + this.getFormattedNameFromSetting = function (setting) { + return _init() + .then(function () { + var name = _formattedNames[this.stripContentType(setting)]; + return name ? name : ''; + }.bind(this)); } - function getSettingFromFormattedName(name) { - if (!_initialized) { - _init(); - } - - for (var setting in _formattedNames) { - if (_formattedNames[setting] == name) { - return setting; + this.getSettingFromFormattedName = function (name) { + return _init() + .then(function () { + for (var setting in _formattedNames) { + if (_formattedNames[setting] == name) { + return setting; + } } - } - - return ''; + return ''; + }); } @@ -346,26 +365,4 @@ Zotero.QuickCopy = new function() { throw ("Invalid mode '" + mode + "' in Zotero.QuickCopy.getContentFromItems()"); } - - - function _init() { - var translation = new Zotero.Translate.Export; - var translators = translation.getTranslators(); - - // add styles to list - var styles = Zotero.Styles.getVisible(); - for each(var style in styles) { - _formattedNames['bibliography=' + style.styleID] = style.title; - } - - for (var i=0; i<translators.length; i++) { - // Skip RDF formats - switch (translators[i].translatorID) { - case '6e372642-ed9d-4934-b5d1-c11ac758ebb7': - case '14763d24-8ba0-45df-8f52-b8d1108e7ac9': - continue; - } - _formattedNames['export=' + translators[i].translatorID] = translators[i].label; - } - } } diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -1033,7 +1033,7 @@ Zotero.Schema = new function(){ var translatorsDir = Zotero.getTranslatorsDirectory(); translatorsDir.remove(true); Zotero.getTranslatorsDirectory(); // recreate directory - return Zotero.Translators.init() + return Zotero.Translators.reinit() .then(function () self.updateBundledFiles('translators', null, false)) .then(function () { var stylesDir = Zotero.getStylesDirectory(); @@ -1058,7 +1058,7 @@ Zotero.Schema = new function(){ var translatorsDir = Zotero.getTranslatorsDirectory(); translatorsDir.remove(true); Zotero.getTranslatorsDirectory(); // recreate directory - return Zotero.Translators.init() + return Zotero.Translators.reinit() .then(function () self.updateBundledFiles('translators', null, true)) .then(callback); } @@ -1535,7 +1535,7 @@ Zotero.Schema = new function(){ } // Rebuild caches - yield Zotero.Translators.init(); + yield Zotero.Translators.reinit(); Zotero.Styles.init(); } catch (e) { diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js @@ -87,15 +87,20 @@ Zotero.Server.Connector.GetTranslators.prototype = { */ "init":function(data, sendResponseCallback) { // Translator data + var me = this; if(data.url) { - var me = this; - Zotero.Translators.getWebTranslatorsForLocation(data.url, function(data) { + Zotero.Translators.getWebTranslatorsForLocation(data.url).then(function(data) { sendResponseCallback(200, "application/json", JSON.stringify(me._serializeTranslators(data[0]))); }); } else { - var responseData = this._serializeTranslators(Zotero.Translators.getAll()); - sendResponseCallback(200, "application/json", JSON.stringify(responseData)); + Zotero.Translators.getAll().then(function(translators) { + var responseData = me._serializeTranslators(translators); + sendResponseCallback(200, "application/json", JSON.stringify(responseData)); + }).fail(function(e) { + sendResponseCallback(500); + throw e; + }).done(); } }, diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js @@ -934,8 +934,6 @@ Zotero.Sync.Storage = new function () { throw new Task.Result(changed); } - Components.utils.import("resource://gre/modules/osfile.jsm"); - let checkItems = function () { if (!items.length) return Q(); diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js @@ -295,13 +295,8 @@ Zotero.Translate.Sandbox = { var translatorsHandlerSet = false; safeTranslator.getTranslators = function() { if(!translation._handlers["translators"] || !translation._handlers["translators"].length) { - if(Zotero.isConnector) { - throw new Error('Translator must register a "translators" handler to '+ - 'call getTranslators() in this translation environment.'); - } else { - translate._debug('COMPAT WARNING: Translator must register a "translators" handler to '+ - 'call getTranslators() in connector'); - } + throw new Error('Translator must register a "translators" handler to '+ + 'call getTranslators() in this translation environment.'); } if(!translatorsHandlerSet) { translation.setHandler("translators", function() { @@ -331,81 +326,57 @@ Zotero.Translate.Sandbox = { if(callback) { translate.incrementAsyncProcesses("safeTranslator#getTranslatorObject()"); } else { - translate._debug("COMPAT WARNING: Translator must pass a callback to getTranslatorObject() to operate in connector"); + throw new Error("Translator must pass a callback to getTranslatorObject() to "+ + "operate in this translation environment."); } - var sandbox; - var haveTranslatorFunction = function(translator) { - translation.translator[0] = translator; - translation._loadTranslator(translator, function() { - if(Zotero.isFx && !Zotero.isBookmarklet) { - // 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 new Error("getTranslatorObject() may not be called from web or search "+ - "translators to web or search translators from different origins."); - } - } + var translator = translation.translator[0]; + (typeof translator === "object" ? Q(translator) : Zotero.Translators.get(translator)). + then(function(translator) { + return translation._loadTranslator(translator); + }).then(function() { + if(Zotero.isFx && !Zotero.isBookmarklet) { + // 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); - translation._prepareTranslation(); - setDefaultHandlers(translate, translation); - sandbox = translation._sandboxManager.sandbox; - if(!Zotero.Utilities.isEmpty(sandbox.exports)) { - sandbox.exports.Zotero = sandbox.Zotero; - sandbox = sandbox.exports; - } else { - translate._debug("COMPAT WARNING: "+translation.translator[0].label+" does "+ - "not export any properties. Only detect"+translation._entryFunctionSuffix+ - " and do"+translation._entryFunctionSuffix+" will be available in "+ - "connectors."); - } + 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); - if(callback) { - try { - callback(sandbox); - } catch(e) { - translate.complete(false, e); - return; - } - translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()"); + try { + secMan.checkSameOriginURI(outerSandboxURI, innerSandboxURI, false); + } catch(e) { + throw new Error("getTranslatorObject() may not be called from web or search "+ + "translators to web or search translators from different origins."); + return; } - }); - }; - - if(typeof translation.translator[0] === "object") { - haveTranslatorFunction(translation.translator[0]); - return translation._sandboxManager.sandbox; - } else { - if(Zotero.isConnector && (!Zotero.isFx || Zotero.isBookmarklet) && !callback) { - throw new Error("Translator must pass a callback to getTranslatorObject() to "+ - "operate in this translation environment."); } - Zotero.Translators.get(translation.translator[0], haveTranslatorFunction); - if(Zotero.isConnector && Zotero.isFx && !callback) { - while(!sandbox && translate._currentState) { - // This processNextEvent call is used to handle a deprecated case - Zotero.mainThread.processNextEvent(true); - } + translation._prepareTranslation(); + setDefaultHandlers(translate, translation); + sandbox = translation._sandboxManager.sandbox; + if(!Zotero.Utilities.isEmpty(sandbox.exports)) { + sandbox.exports.Zotero = sandbox.Zotero; + sandbox = sandbox.exports; + } else { + translate._debug("COMPAT WARNING: "+translation.translator[0].label+" does "+ + "not export any properties. Only detect"+translation._entryFunctionSuffix+ + " and do"+translation._entryFunctionSuffix+" will be available in "+ + "connectors."); } - if(sandbox) return sandbox; - } + + callback(sandbox); + translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()"); + }).fail(function(e) { + translate.complete(false, e); + return; + }); }; - // TODO security is not super-tight here, as someone could pass something into arg - // that gets evaluated in the wrong scope in Fx < 4. We should wrap this. - return safeTranslator; }, @@ -1010,9 +981,11 @@ Zotero.Translate.Base.prototype = { * @param {Boolean} [checkSetTranslator] If true, the appropriate detect function is run on the * set document/text/etc. using the translator set by setTranslator. * getAllTranslators parameter is meaningless in this context. - * @return {Zotero.Translator[]} An array of {@link Zotero.Translator} objects + * @return {Promise} Promise for an array of {@link Zotero.Translator} objects */ "getTranslators":function(getAllTranslators, checkSetTranslator) { + var potentialTranslators; + // do not allow simultaneous instances of getTranslators if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); this._currentState = "detect"; @@ -1021,10 +994,10 @@ Zotero.Translate.Base.prototype = { if(checkSetTranslator) { // setTranslator must be called beforehand if checkSetTranslator is set if( !this.translator || !this.translator[0] ) { - throw new Error("getTranslators: translator must be set via setTranslator before calling" + - " getTranslators with the checkSetTranslator flag"); + return Q.reject(new Error("getTranslators: translator must be set via setTranslator before calling" + + " getTranslators with the checkSetTranslator flag")); } - var translators = new Array(); + var promises = new Array(); var t; for(var i=0, n=this.translator.length; i<n; i++) { if(typeof(this.translator[i]) == 'string') { @@ -1034,80 +1007,83 @@ Zotero.Translate.Base.prototype = { t = this.translator[i]; } /**TODO: check that the translator is of appropriate type?*/ - if(t) translators.push(t); + if(t) promises.push(t); } - if(!translators.length) throw new Error("getTranslators: no valid translators were set."); - this._getTranslatorsTranslatorsReceived(translators); + if(!promises.length) return Q.reject(new Error("getTranslators: no valid translators were set")); + potentialTranslators = Q.all(promises); } else { - this._getTranslatorsGetPotentialTranslators(); + potentialTranslators = 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 = []; - - // 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; - - for(var i=0, n=allPotentialTranslators.length; i<n; i++) { - var translator = allPotentialTranslators[i]; - if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { - this._potentialTranslators.push(translator); - } else if(this instanceof Zotero.Translate.Web && Zotero.Connector) { - this._waitingForRPC = true; + return potentialTranslators.spread(function(allPotentialTranslators, properToProxyFunctions) { + me._potentialTranslators = []; + me._foundTranslators = []; + + // 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. + me._properToProxyFunctions = properToProxyFunctions ? properToProxyFunctions : null; + me._waitingForRPC = false; + + for(var i=0, n=allPotentialTranslators.length; i<n; i++) { + var translator = allPotentialTranslators[i]; + if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER) { + me._potentialTranslators.push(translator); + } else if(me instanceof Zotero.Translate.Web && Zotero.Connector) { + me._waitingForRPC = true; + } } - } - - 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(); + + // Attach handler for translators, so that we can return a + // promise that provides them. + // TODO make me._detect() return a promise + var deferred = Q.defer(), + translatorsHandler = function(obj, translators) { + me.removeHandler("translators", translatorsHandler); + deferred.resolve(translators); + } + me.setHandler("translators", translatorsHandler); + me._detect(); + + if(me._waitingForRPC) { + // Try detect in Zotero Standalone. If this fails, it fails; we shouldn't + // get hung up about it. + Zotero.Connector.callMethod("detect", {"uri":me.location.toString(), + "cookie":me.document.cookie, + "html":me.document.documentElement.innerHTML}).then(function(rpcTranslators) { + me._waitingForRPC = false; + + // if there are translators, add them to the list of found translators + if(rpcTranslators) { + for(var i=0, n=rpcTranslators.length; i<n; i++) { + rpcTranslators[i].runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE; + } + me._foundTranslators = me._foundTranslators.concat(rpcTranslators); + } + + // call _detectTranslatorsCollected to return detected translators + if(me._currentState === null) { + me._detectTranslatorsCollected(); + } + }); + } + + return deferred.promise; + }).fail(function(e) { + Zotero.logError(e); + me.complete(false, e); + }); }, - + /** - * Called on completion of detect RPC for - * {@link Zotero.Translate.Base#_getTranslatorsTranslatorsReceived} + * Get all potential translators (without running detect) + * @return {Promise} Promise for an array of {@link Zotero.Translator} objects */ - "_getTranslatorsRPCComplete":function(rpcTranslators) { - this._waitingForRPC = false; - - // if there are translators, add them to the list of found translators - if(rpcTranslators) { - for(var i=0, n=rpcTranslators.length; i<n; i++) { - rpcTranslators[i].runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE; - } - this._foundTranslators = this._foundTranslators.concat(rpcTranslators); - } - - // call _detectTranslatorsCollected to return detected translators - if(this._currentState === null) { - this._detectTranslatorsCollected(); - } + "_getTranslatorsGetPotentialTranslators":function() { + return Zotero.Translators.getAllForType(this.type). + then(function(translators) { return [translators] }); }, /** @@ -1136,14 +1112,14 @@ Zotero.Translate.Base.prototype = { var me = this; if(typeof this.translator[0] === "object") { // already have a translator object, so use it - this._loadTranslator(this.translator[0], function() { me._translateTranslatorLoaded() }); + this._loadTranslator(this.translator[0]).then(function() { me._translateTranslatorLoaded() }); } else { // need to get translator first - Zotero.Translators.get(this.translator[0], - function(translator) { - me.translator[0] = translator; - me._loadTranslator(translator, function() { me._translateTranslatorLoaded() }); - }); + Zotero.Translators.get(this.translator[0]). + then(function(translator) { + me.translator[0] = translator; + me._loadTranslator(translator).then(function() { me._translateTranslatorLoaded() }); + }); } }, @@ -1151,12 +1127,6 @@ Zotero.Translate.Base.prototype = { * Called when translator has been retrieved and loaded */ "_translateTranslatorLoaded":function() { - if(!this.translator[0].code) { - this.complete(false, - new Error("Translator "+this.translator[0].label+" is unsupported within this environment")); - return; - } - // set display options to default if they don't exist if(!this._displayOptions) this._displayOptions = this._translatorInfo.displayOptions || {}; @@ -1429,8 +1399,8 @@ Zotero.Translate.Base.prototype = { } var me = this; - this._loadTranslator(this._potentialTranslators[0], - function() { me._detectTranslatorLoaded() }); + this._loadTranslator(this._potentialTranslators[0]). + then(function() { me._detectTranslatorLoaded() }); }, /** @@ -1466,7 +1436,7 @@ Zotero.Translate.Base.prototype = { * @param {Zotero.Translator} translator * @return {Boolean} Whether the translator could be successfully loaded */ - "_loadTranslator":function(translator, callback) { + "_loadTranslator":function(translator) { var sandboxLocation = this._getSandboxLocation(); if(!this._sandboxLocation || sandboxLocation !== this._sandboxLocation) { this._sandboxLocation = sandboxLocation; @@ -1479,20 +1449,17 @@ Zotero.Translate.Base.prototype = { this._aborted = false; this.saveQueue = []; - Zotero.debug("Translate: Parsing code for "+translator.label, 4); - - try { - this._sandboxManager.eval("var exports = {}, ZOTERO_TRANSLATOR_INFO = "+translator.code, - ["detect"+this._entryFunctionSuffix, "do"+this._entryFunctionSuffix, "exports", + var me = this; + return translator.getCode().then(function(code) { + Zotero.debug("Translate: Parsing code for "+translator.label, 4); + me._sandboxManager.eval("var exports = {}, ZOTERO_TRANSLATOR_INFO = "+code, + ["detect"+me._entryFunctionSuffix, "do"+me._entryFunctionSuffix, "exports", "ZOTERO_TRANSLATOR_INFO"], (translator.file ? translator.file.path : translator.label)); - } catch(e) { - this.complete(false, e); - return; - } - this._translatorInfo = this._sandboxManager.sandbox.ZOTERO_TRANSLATOR_INFO; - - if(callback) callback(); + me._translatorInfo = me._sandboxManager.sandbox.ZOTERO_TRANSLATOR_INFO; + }).fail(function(e) { + me.complete(false, e); + }); }, /** @@ -1682,13 +1649,7 @@ Zotero.Translate.Web.prototype.setLocation = function(location) { * Get potential web translators */ 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]); - }); + return Zotero.Translators.getWebTranslatorsForLocation(this.location); } /** @@ -1920,13 +1881,10 @@ Zotero.Translate.Import.prototype.complete = function(returnValue, error) { * Get all potential import translators, ordering translators with the right file extension first */ Zotero.Translate.Import.prototype._getTranslatorsGetPotentialTranslators = function() { - if(this.location) { - var me = this; - Zotero.Translators.getImportTranslatorsForLocation(this.location, - function(translators) { me._getTranslatorsTranslatorsReceived(translators) }); - } else { - Zotero.Translate.Base.prototype._getTranslatorsGetPotentialTranslators.call(this); - } + return (this.location ? + Zotero.Translators.getImportTranslatorsForLocation(this.location) : + Zotero.Translators.getAllForType(this.type)). + then(function(translators) { return [translators] });; } /** @@ -1938,12 +1896,13 @@ Zotero.Translate.Import.prototype.getTranslators = function() { if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); this._currentState = "detect"; var me = this; - Zotero.Translators.getAllForType(this.type, function(translators) { + return Zotero.Translators.getAllForType(this.type). + then(function(translators) { me._potentialTranslators = []; me._foundTranslators = translators; me.complete(true); + return me._foundTranslators; }); - if(this._currentState === null) return this._foundTranslators; } else { return Zotero.Translate.Base.prototype.getTranslators.call(this); } @@ -1955,7 +1914,8 @@ Zotero.Translate.Import.prototype.getTranslators = function() { Zotero.Translate.Import.prototype._loadTranslator = function(translator, callback) { // call super var me = this; - Zotero.Translate.Base.prototype._loadTranslator.call(this, translator, function() { + return Zotero.Translate.Base.prototype._loadTranslator.call(this, translator). + then(function() { me._loadTranslatorPrepareIO(translator, callback); }); } @@ -2113,12 +2073,17 @@ Zotero.Translate.Export.prototype.complete = function(returnValue, error) { * Overload {@link Zotero.Translate.Base#getTranslators} to return all translators immediately */ Zotero.Translate.Export.prototype.getTranslators = function() { - if(this._currentState === "detect") throw new Error("getTranslators: detection is already running"); - this._currentState = "detect"; - this._foundTranslators = Zotero.Translators.getAllForType(this.type); - this._potentialTranslators = []; - this.complete(true); - return this._foundTranslators; + if(this._currentState === "detect") { + return Q.reject(new Error("getTranslators: detection is already running")); + } + var me = this; + return Zotero.Translators.getAllForType(this.type).then(function(translators) { + me._currentState = "detect"; + me._foundTranslators = translators; + me._potentialTranslators = []; + me.complete(true); + return me._foundTranslators; + }); } /** diff --git a/chrome/content/zotero/xpcom/translation/translator.js b/chrome/content/zotero/xpcom/translation/translator.js @@ -1,7 +1,7 @@ /* ***** BEGIN LICENSE BLOCK ***** - Copyright © 2009 Center for History and New Media + Copyright © 2013 Center for History and New Media George Mason University, Fairfax, Virginia, USA http://zotero.org @@ -23,398 +23,22 @@ ***** 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 = Q.async(function() { - _initialized = true; - - var start = (new Date()).getTime(); - var transactionStarted = false; - - _cache = {"import":[], "export":[], "web":[], "search":[]}; - _translators = {}; - - var dbCacheResults = yield Zotero.DB.queryAsync("SELECT leafName, translatorJSON, "+ - "code, lastModifiedTime FROM translatorCache"); - var dbCache = {}; - for each(var cacheEntry in dbCacheResults) { - dbCache[cacheEntry.leafName] = cacheEntry; - } - - var i = 0; - var filesInCache = {}; - var contents = Zotero.getTranslatorsDirectory().directoryEntries; - while(contents.hasMoreElements()) { - var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile); - var leafName = file.leafName; - if(!(/^[^.].*\.js$/.test(leafName))) continue; - var lastModifiedTime = file.lastModifiedTime; - - var dbCacheEntry = false; - if(dbCache[leafName]) { - filesInCache[leafName] = true; - if(dbCache[leafName].lastModifiedTime == lastModifiedTime) { - dbCacheEntry = dbCache[file.leafName]; - } - } - - // TODO: use async load method instead of constructor - if(dbCacheEntry) { - // get JSON from cache if possible - var translator = new Zotero.Translator(file, dbCacheEntry.translatorJSON, dbCacheEntry.code); - filesInCache[leafName] = true; - } else { - // otherwise, load from file - var translator = new Zotero.Translator(file); - } - - if(translator.translatorID) { - if(_translators[translator.translatorID]) { - // same translator is already cached - translator.logError('Translator with ID '+ - translator.translatorID+' already loaded from "'+ - _translators[translator.translatorID].file.leafName+'"'); - } else { - // add to cache - _translators[translator.translatorID] = translator; - for(var type in TRANSLATOR_TYPES) { - if(translator.translatorType & TRANSLATOR_TYPES[type]) { - _cache[type].push(translator); - } - } - - if(!dbCacheEntry) { - yield Zotero.Translators.cacheInDB( - leafName, - translator.metadataString, - translator.cacheCode ? translator.code : null, - lastModifiedTime - ); - delete translator.metadataString; - } - } - } - - i++; - } - - // Remove translators from DB as necessary - for(var leafName in dbCache) { - if(!filesInCache[leafName]) { - yield Zotero.DB.queryAsync( - "DELETE FROM translatorCache WHERE leafName = ?", [leafName] - ); - } - } - - // Sort by priority - var collation = Zotero.getLocaleCollation(); - var cmp = function (a, b) { - if (a.priority > b.priority) { - return 1; - } - else if (a.priority < b.priority) { - return -1; - } - return collation.compareString(1, a.label, b.label); - } - for(var type in _cache) { - _cache[type].sort(cmp); - } - - Zotero.debug("Cached "+i+" translators in "+((new Date()).getTime() - start)+" ms"); - }); - - /** - * 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) this.init(); - 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.getAll = function() { - if(!_initialized) this.init(); - 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) { - // First, drop the 0- if it exists (this is an III invention) - var host = m[2]; - if(host.substr(0, 2) === "0-") host = host.substr(2); - var hostnames = host.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 && 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; - } - - /** - * @param {String} label - * @return {String} - */ - this.getFileNameFromLabel = function(label, alternative) { - var fileName = Zotero.Utilities.removeDiacritics( - Zotero.File.getValidFileName(label)) + ".js"; - // Use translatorID if name still isn't ASCII (e.g., Cyrillic) - if (alternative && !fileName.match(/^[\x00-\x7f]+$/)) { - fileName = alternative + ".js"; - } - return fileName; - } - - /** - * @param {String} metadata - * @param {String} metadata.translatorID Translator GUID - * @param {Integer} metadata.translatorType See TRANSLATOR_TYPES in translate.js - * @param {String} metadata.label Translator title - * @param {String} metadata.creator Translator author - * @param {String|Null} metadata.target Target regexp - * @param {String|Null} metadata.minVersion - * @param {String} metadata.maxVersion - * @param {String|undefined} metadata.configOptions - * @param {String|undefined} metadata.displayOptions - * @param {Integer} metadata.priority - * @param {String} metadata.browserSupport - * @param {Boolean} metadata.inRepository - * @param {String} metadata.lastUpdated SQL date - * @param {String} code - * @return {Promise<nsIFile>} - */ - this.save = function(metadata, code) { - if (!metadata.translatorID) { - throw ("metadata.translatorID not provided in Zotero.Translators.save()"); - } - - if (!metadata.translatorType) { - var found = false; - for each(var type in TRANSLATOR_TYPES) { - if (metadata.translatorType & type) { - found = true; - break; - } - } - if (!found) { - throw ("Invalid translatorType '" + metadata.translatorType + "' in Zotero.Translators.save()"); - } - } - - if (!metadata.label) { - throw ("metadata.label not provided in Zotero.Translators.save()"); - } - - if (!metadata.priority) { - throw ("metadata.priority not provided in Zotero.Translators.save()"); - } - - if (!metadata.lastUpdated) { - throw ("metadata.lastUpdated not provided in Zotero.Translators.save()"); - } - - if (!code) { - throw ("code not provided in Zotero.Translators.save()"); - } - - var fileName = Zotero.Translators.getFileNameFromLabel( - metadata.label, metadata.translatorID - ); - var destFile = Zotero.getTranslatorsDirectory(); - destFile.append(fileName); - - // JSON.stringify has the benefit of indenting JSON - var metadataJSON = JSON.stringify(metadata, null, "\t"); - - var str = metadataJSON + "\n\n" + code; - - var translator = Zotero.Translators.get(metadata.translatorID); - if (translator && destFile.equals(translator.file)) { - var sameFile = true; - } - - return Q.fcall(function () { - if (sameFile) return; - - return Q(OS.File.exists(destFile.path)) - .then(function (exists) { - if (exists) { - var msg = "Overwriting translator with same filename '" - + fileName + "'"; - Zotero.debug(msg, 1); - Zotero.debug(metadata, 1); - Components.utils.reportError(msg); - } - }); - }) - .then(function () { - if (!translator) return; - - return Q(OS.File.exists(translator.file.path)) - .then(function (exists) { - translator.file.remove(false); - }); - }) - .then(function () { - Zotero.debug("Saving translator '" + metadata.label + "'"); - Zotero.debug(str); - return Zotero.File.putContentsAsync(destFile, str) - .thenResolve(destFile); - }); - } - - this.cacheInDB = function(fileName, metadataJSON, code, lastModifiedTime) { - return Zotero.DB.queryAsync( - "REPLACE INTO translatorCache VALUES (?, ?, ?, ?)", - [fileName, metadataJSON, code, lastModifiedTime] - ); - } -} +// Properties required for every translator +var TRANSLATOR_REQUIRED_PROPERTIES = ["translatorID", "translatorType", "label", "creator", + "target", "priority", "lastUpdated"]; +// Properties that are preserved if present +var TRANSLATOR_OPTIONAL_PROPERTIES = ["browserSupport", "minVersion", "maxVersion", + "inRepository", "configOptions", "displayOptions", + "hiddenPrefs"]; +// Properties that are passed from background to inject page in connector +var TRANSLATOR_PASSING_PROPERTIES = TRANSLATOR_REQUIRED_PROPERTIES. + concat(["browserSupport", "code", "runMode"]); +// Properties that are saved in connector if set but not required +var TRANSLATOR_SAVE_PROPERTIES = TRANSLATOR_REQUIRED_PROPERTIES.concat(["browserSupport"]); /** * @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 @@ -436,124 +60,118 @@ Zotero.Translators = new function() { * @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 + * @property {Boolean} cacheCode Whether to cache code for this session (non-connector only) + * @property {nsIFile} [file] File corresponding to this translator (non-connector only) */ -Zotero.Translator = function(file, json, code) { - const codeGetterFunction = function() { return Zotero.File.getContents(this.file); } - // Maximum length for the info JSON in a translator - const MAX_INFO_LENGTH = 4096; - const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/; - - this.file = file; - - var fStream, cStream; - if(json) { - var info = JSON.parse(json); - } else { - fStream = Components.classes["@mozilla.org/network/file-input-stream;1"]. - createInstance(Components.interfaces.nsIFileInputStream); - cStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]. - createInstance(Components.interfaces.nsIConverterInputStream); - fStream.init(file, -1, -1, 0); - cStream.init(fStream, "UTF-8", 8192, - Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); - - var str = {}; - cStream.readString(MAX_INFO_LENGTH, str); - - var m = infoRe.exec(str.value); - if (!m) { - this.logError("Invalid or missing translator metadata JSON object in " + file.leafName); - fStream.close(); - return; - } - - this.metadataString = m[0]; - - try { - var info = JSON.parse(this.metadataString); - } catch(e) { - this.logError("Invalid or missing translator metadata JSON object in " + file.leafName); - fStream.close(); - return; - } - } - - var haveMetadata = true; +Zotero.Translator = function(info) { + this.init(info); +} + +/** + * Initializes a translator from a set of info, clearing code if it is set + */ +Zotero.Translator.prototype.init = function(info) { // make sure we have all the properties - for each(var property in ["translatorID", "translatorType", "label", "creator", "target", "minVersion", "maxVersion", "priority", "lastUpdated", "inRepository"]) { + for(var i=0; i<TRANSLATOR_REQUIRED_PROPERTIES.length; i++) { + var property = TRANSLATOR_REQUIRED_PROPERTIES[i]; if(info[property] === undefined) { - this.logError('Missing property "'+property+'" in translator metadata JSON object in ' + file.leafName); + this.logError(new Error('Missing property "'+property+'" in translator metadata JSON object in ' + info.label)); haveMetadata = false; break; } else { this[property] = info[property]; } } - if(!haveMetadata) { - if(fStream) fStream.close(); - return; + for(var i=0; i<TRANSLATOR_OPTIONAL_PROPERTIES.length; i++) { + var property = TRANSLATOR_OPTIONAL_PROPERTIES[i]; + if(info[property] !== undefined) { + this[property] = info[property]; + } } - this._configOptions = info["configOptions"] ? info["configOptions"] : {}; - this._displayOptions = info["displayOptions"] ? info["displayOptions"] : {}; - this._hiddenPrefs = info["hiddenPrefs"] ? info["hiddenPrefs"] : {}; - this.browserSupport = info["browserSupport"] ? info["browserSupport"] : "g"; - this.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER; + + var supported = ( + Zotero.isBookmarklet ? + (this.browserSupport.indexOf(Zotero.browser) !== -1 && this.browserSupport.indexOf("b") !== -1) || + /(?:^|; ?)bookmarklet-debug-mode=1(?:$|; ?)/.test(document.cookie) : + this.browserSupport.indexOf(Zotero.browser) !== -1); + + if(supported) { + this.runMode = Zotero.Translator.RUN_MODE_IN_BROWSER; + } else { + this.runMode = Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE; + } if(this.translatorType & TRANSLATOR_TYPES["import"]) { // compile import regexp to match only file extension - try { - this.importRegexp = this.target ? new RegExp("\\."+this.target+"$", "i") : null; - } catch(e) { - this.logError("Invalid target in " + file.leafName); - this.importRegexp = null; - if(fStream) fStream.close(); - return; - } + this.importRegexp = this.target ? new RegExp("\\."+this.target+"$", "i") : null; + } else if(this.hasOwnProperty("importRegexp")) { + delete this.importRegexp; } - - this.cacheCode = false; + + this.cacheCode = Zotero.isConnector; if(this.translatorType & TRANSLATOR_TYPES["web"]) { // compile web regexp - try { - this.webRegexp = this.target ? new RegExp(this.target, "i") : null; - } catch(e) { - this.logError("Invalid target in " + file.leafName); - this.webRegexp = null; - if(fStream) fStream.close(); - return; - } - - if(!this.target) { - this.cacheCode = true; - - if(json) { - // if have JSON, also have code - this.code = code; - } else { - // for translators used on every page, cache code in memory - var strs = [str.value]; - var amountRead; - while(amountRead = cStream.readString(8192, str)) strs.push(str.value); - this.code = strs.join(""); + this.cacheCode |= !this.target; + this.webRegexp = this.target ? new RegExp(this.target, "i") : null; + } else if(this.hasOwnProperty("webRegexp")) { + delete this.webRegexp; + } + + if(info.file) this.file = info.file; + if(info.code && this.cacheCode) { + this.code = info.code; + } else if(this.hasOwnProperty("code")) { + delete this.code; + } +} + +/** + * Load code for a translator + */ +Zotero.Translator.prototype.getCode = function() { + if(this.code) return Q(this.code); + + var me = this; + if(Zotero.isConnector) { + // TODO make this a promise + return Zotero.Repo.getTranslatorCode(this.translatorID). + spread(function(code, source) { + if(!code) { + throw "Code for "+me.label+" could not be retrieved"; } + // Cache any translators for session, since retrieving via + // HTTP may be expensive + me.code = code; + me.codeSource = source; + return code; + }); + } else { + var promise = Zotero.File.getContentsAsync(this.file); + if(this.cacheCode) { + // Cache target-less web translators for session, since we + // will use them a lot + promise.then(function(code) { + me.code = code; + return code; + }); } + return promise; } - - if(!this.cacheCode) this.__defineGetter__("code", codeGetterFunction); - if(!json) cStream.close(); } -Zotero.Translator.prototype.__defineGetter__("displayOptions", function() { - return Zotero.Utilities.deepCopy(this._displayOptions); -}); -Zotero.Translator.prototype.__defineGetter__("configOptions", function() { - return Zotero.Utilities.deepCopy(this._configOptions); -}); -Zotero.Translator.prototype.__defineGetter__("hiddenPrefs", function() { - return Zotero.Utilities.deepCopy(this._hiddenPrefs); -}); +/** + * Get metadata block for a translator + */ +Zotero.Translator.prototype.serialize = function(properties) { + var info = {}; + for(var i in properties) { + var property = properties[i]; + info[property] = translator[property]; + } + return info; +} /** * Log a translator-related error @@ -564,9 +182,13 @@ Zotero.Translator.prototype.__defineGetter__("hiddenPrefs", function() { * @param {Integer} colNumber */ Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, colNumber) { - 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); + if(Zotero.isFx && this.file) { + 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); + } else { + Zotero.logError(message); + } } Zotero.Translator.RUN_MODE_IN_BROWSER = 1; diff --git a/chrome/content/zotero/xpcom/translation/translators.js b/chrome/content/zotero/xpcom/translation/translators.js @@ -0,0 +1,422 @@ +/* + ***** 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.reinit = Q.async(function() { + var start = (new Date()).getTime(); + var transactionStarted = false; + + _cache = {"import":[], "export":[], "web":[], "search":[]}; + _translators = {}; + + var dbCacheResults = yield Zotero.DB.queryAsync("SELECT leafName, translatorJSON, "+ + "code, lastModifiedTime FROM translatorCache"); + var dbCache = {}; + for each(var cacheEntry in dbCacheResults) { + dbCache[cacheEntry.leafName] = cacheEntry; + } + + var i = 0; + var filesInCache = {}; + var contents = Zotero.getTranslatorsDirectory().directoryEntries; + while(contents.hasMoreElements()) { + var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile); + var leafName = file.leafName; + if(!(/^[^.].*\.js$/.test(leafName))) continue; + var lastModifiedTime = file.lastModifiedTime; + + var dbCacheEntry = false; + if(dbCache[leafName]) { + filesInCache[leafName] = true; + if(dbCache[leafName].lastModifiedTime == lastModifiedTime) { + dbCacheEntry = dbCache[file.leafName]; + } + } + + if(dbCacheEntry) { + // get JSON from cache if possible + var translator = Zotero.Translators.load(file, dbCacheEntry.translatorJSON, dbCacheEntry.code); + filesInCache[leafName] = true; + } else { + // otherwise, load from file + var translator = yield Zotero.Translators.loadFromFile(file); + } + + if(translator.translatorID) { + if(_translators[translator.translatorID]) { + // same translator is already cached + translator.logError('Translator with ID '+ + translator.translatorID+' already loaded from "'+ + _translators[translator.translatorID].file.leafName+'"'); + } else { + // add to cache + _translators[translator.translatorID] = translator; + for(var type in TRANSLATOR_TYPES) { + if(translator.translatorType & TRANSLATOR_TYPES[type]) { + _cache[type].push(translator); + } + } + + if(!dbCacheEntry) { + var code = yield translator.getCode(); + yield Zotero.Translators.cacheInDB( + leafName, + translator.serialize(TRANSLATOR_REQUIRED_PROPERTIES. + concat(TRANSLATOR_OPTIONAL_PROPERTIES)), + translator.cacheCode ? translator.code : null, + lastModifiedTime + ); + delete translator.metadataString; + } + } + } + + i++; + } + + // Remove translators from DB as necessary + for(var leafName in dbCache) { + if(!filesInCache[leafName]) { + yield Zotero.DB.queryAsync( + "DELETE FROM translatorCache WHERE leafName = ?", [leafName] + ); + } + } + + // Sort by priority + var collation = Zotero.getLocaleCollation(); + var cmp = function (a, b) { + if (a.priority > b.priority) { + return 1; + } + else if (a.priority < b.priority) { + return -1; + } + return collation.compareString(1, a.label, b.label); + } + for(var type in _cache) { + _cache[type].sort(cmp); + } + + Zotero.debug("Cached "+i+" translators in "+((new Date()).getTime() - start)+" ms"); + }); + this.init = Zotero.lazy(this.reinit); + + /** + * Loads a translator from JSON, with optional code + */ + this.load = function(file, json, code) { + var info = JSON.parse(json); + info.file = file; + info.code = code; + return new Zotero.Translator(info); + } + + /** + * Loads a translator from the disk + */ + this.loadFromDisk = function(file) { + const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/; + Zotero.File.getContentsAsync(this.file).then(function(source) { + return Zotero.Translators.load(file, infoRe.exec(source)[0], source); + }).fail(function() { + throw "Invalid or missing translator metadata JSON object in " + file.leafName; + }); + } + + /** + * 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) { + return this.init().then(function() { + return _translators[id] ? _translators[id] : false + }); + } + + /** + * 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) { + return this.init().then(function() { + return _cache[type].slice(); + }); + } + + /** + * Gets all translators for a specific type of translation + */ + this.getAll = function() { + return this.init().then(function() { + 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) { + return this.getAllForType("web").then(function(allTranslators) { + 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) { + // First, drop the 0- if it exists (this is an III invention) + var host = m[2]; + if(host.substr(0, 2) === "0-") host = host.substr(2); + var hostnames = host.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; + } + } + } + + return [potentialTranslators, converterFunctions]; + }); + } + + /** + * 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) { + return Zotero.Translators.getAllForType("import").then(function(allTranslators) { + var tier1Translators = []; + var tier2Translators = []; + + for(var i=0; i<allTranslators.length; i++) { + if(allTranslators[i].importRegexp && 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; + }); + } + + /** + * @param {String} label + * @return {String} + */ + this.getFileNameFromLabel = function(label, alternative) { + var fileName = Zotero.Utilities.removeDiacritics( + Zotero.File.getValidFileName(label)) + ".js"; + // Use translatorID if name still isn't ASCII (e.g., Cyrillic) + if (alternative && !fileName.match(/^[\x00-\x7f]+$/)) { + fileName = alternative + ".js"; + } + return fileName; + } + + /** + * @param {String} metadata + * @param {String} metadata.translatorID Translator GUID + * @param {Integer} metadata.translatorType See TRANSLATOR_TYPES in translate.js + * @param {String} metadata.label Translator title + * @param {String} metadata.creator Translator author + * @param {String|Null} metadata.target Target regexp + * @param {String|Null} metadata.minVersion + * @param {String} metadata.maxVersion + * @param {String|undefined} metadata.configOptions + * @param {String|undefined} metadata.displayOptions + * @param {Integer} metadata.priority + * @param {String} metadata.browserSupport + * @param {Boolean} metadata.inRepository + * @param {String} metadata.lastUpdated SQL date + * @param {String} code + * @return {Promise<nsIFile>} + */ + this.save = function(metadata, code) { + if (!metadata.translatorID) { + throw ("metadata.translatorID not provided in Zotero.Translators.save()"); + } + + if (!metadata.translatorType) { + var found = false; + for each(var type in TRANSLATOR_TYPES) { + if (metadata.translatorType & type) { + found = true; + break; + } + } + if (!found) { + throw ("Invalid translatorType '" + metadata.translatorType + "' in Zotero.Translators.save()"); + } + } + + if (!metadata.label) { + throw ("metadata.label not provided in Zotero.Translators.save()"); + } + + if (!metadata.priority) { + throw ("metadata.priority not provided in Zotero.Translators.save()"); + } + + if (!metadata.lastUpdated) { + throw ("metadata.lastUpdated not provided in Zotero.Translators.save()"); + } + + if (!code) { + throw ("code not provided in Zotero.Translators.save()"); + } + + var fileName = Zotero.Translators.getFileNameFromLabel( + metadata.label, metadata.translatorID + ); + var destFile = Zotero.getTranslatorsDirectory(); + destFile.append(fileName); + + // JSON.stringify has the benefit of indenting JSON + var metadataJSON = JSON.stringify(metadata, null, "\t"); + + var str = metadataJSON + "\n\n" + code, + translator; + + return Zotero.Translators.get(metadata.translatorID) + .then(function(gTranslator) { + translator = gTranslator; + var sameFile = translator && destFile.equals(translator.file); + if (sameFile) return; + + return Q(OS.File.exists(destFile.path)) + .then(function (exists) { + if (exists) { + var msg = "Overwriting translator with same filename '" + + fileName + "'"; + Zotero.debug(msg, 1); + Zotero.debug(metadata, 1); + Components.utils.reportError(msg); + } + }); + }) + .then(function () { + if (!translator) return; + + return Q(OS.File.exists(translator.file.path)) + .then(function (exists) { + translator.file.remove(false); + }); + }) + .then(function () { + Zotero.debug("Saving translator '" + metadata.label + "'"); + Zotero.debug(str); + return Zotero.File.putContentsAsync(destFile, str) + .thenResolve(destFile); + }); + } + + this.cacheInDB = function(fileName, metadataJSON, code, lastModifiedTime) { + return Zotero.DB.queryAsync( + "REPLACE INTO translatorCache VALUES (?, ?, ?, ?)", + [fileName, metadataJSON, code, lastModifiedTime] + ); + } +} diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -96,7 +96,6 @@ Zotero.Utilities.Internal = { * rather than hex string */ "md5Async": function (file, base64) { - Components.utils.import("resource://gre/modules/osfile.jsm"); const CHUNK_SIZE = 16384; var deferred = Q.defer(); diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -44,6 +44,7 @@ const ZOTERO_CONFIG = { Components.utils.import("resource://zotero/q.js"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/osfile.jsm"); /* * Core functions diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -44,6 +44,7 @@ const xpcomFilesAll = [ 'progressWindow', 'translation/translate', 'translation/translate_firefox', + 'translation/translator', 'translation/tlds', 'utilities', 'utilities_internal', @@ -104,7 +105,7 @@ const xpcomFilesLocal = [ 'timeline', 'uri', 'translation/translate_item', - 'translation/translator', + 'translation/translators', 'server_connector' ];