commit 526441c3ca104cdbcf016cc29f087567a1f3c22b
parent efc44554765945b5cbd11ac20f3967f9e6916136
Author: Simon Kornblith <simon@simonster.com>
Date: Sat, 17 Aug 2013 03:08:59 -0400
Make translator loading asynchronous
Other changes:
- Factored out Zotero.Translators from Zotero.Translator. The latter
should be usable in the bookmarklet and connectors without changes.
- configOptions, displayOptions, and hiddenPrefs no longer copy on
read. I don't think this actually affects any existing code.
- Zotero.Translate._loadTranslator() now returns a promise
Diffstat:
4 files changed, 585 insertions(+), 535 deletions(-)
diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js
@@ -333,44 +333,44 @@ Zotero.Translate.Sandbox = {
var translator = translation.translator[0];
(typeof translator === "object" ? Q(translator) : Zotero.Translators.get(translator)).
then(function(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.");
- return;
- }
- }
+ 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);
- callback(sandbox);
- 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;
+ }
+ }
+
+ 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.");
+ }
+
+ callback(sandbox);
+ translate.decrementAsyncProcesses("safeTranslator#getTranslatorObject()");
}).fail(function(e) {
translate.complete(false, e);
return;
@@ -1112,13 +1112,13 @@ 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]).
then(function(translator) {
me.translator[0] = translator;
- me._loadTranslator(translator, function() { me._translateTranslatorLoaded() });
+ me._loadTranslator(translator).then(function() { me._translateTranslatorLoaded() });
});
}
},
@@ -1127,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 || {};
@@ -1405,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() });
},
/**
@@ -1442,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;
@@ -1455,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);
+ });
},
/**
@@ -1923,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);
});
}
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,385 +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.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];
- }
- }
-
- // 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");
- });
- this.init = Zotero.lazy(this.reinit);
-
- /**
- * 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]
- );
- }
-}
+// 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
@@ -423,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
@@ -551,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/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'
];