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
This commit is contained in:
Simon Kornblith 2013-08-17 03:08:59 -04:00
parent efc4455476
commit 526441c3ca
4 changed files with 589 additions and 539 deletions

View file

@ -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);
});
}

View file

@ -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.cacheCode = false;
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.importRegexp = this.target ? new RegExp("\\."+this.target+"$", "i") : null;
} else if(this.hasOwnProperty("importRegexp")) {
delete this.importRegexp;
}
if(!this.cacheCode) this.__defineGetter__("code", codeGetterFunction);
if(!json) cStream.close();
this.cacheCode = Zotero.isConnector;
if(this.translatorType & TRANSLATOR_TYPES["web"]) {
// compile web regexp
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;
}
}
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);
});
/**
* 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;
}
}
/**
* 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;

View file

@ -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]
);
}
}

View file

@ -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'
];