zotero/chrome/content/zotero/xpcom/connector/translator.js
Adomas Venčkauskas e83bbbad4e Fix getTranslatorCode bugs
- /connector/getTranslatorCode used to respond with empty content
- CodeGetter froze connectors by issuing hundreds of simultanious XHR
  requests
2016-09-20 15:55:58 +03:00

475 lines
16 KiB
JavaScript

/*
***** 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
var 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
* @param {Zotero.Translate[]} [translators] List of translators. If not specified, it will be
* retrieved from storage.
*/
this.init = function(translators) {
if(!translators) {
translators = [];
if((Zotero.isBrowserExt || Zotero.isSafari) && localStorage["translatorMetadata"]) {
try {
translators = JSON.parse(localStorage["translatorMetadata"]);
if(typeof translators !== "object") {
translators = [];
}
} catch(e) {}
}
}
_cache = {"import":[], "export":[], "web":[], "search":[]};
_translators = {};
_initialized = true;
// Build caches
for(var i=0; i<translators.length; i++) {
try {
var translator = new Zotero.Translator(translators[i]);
_translators[translator.translatorID] = translator;
for(var type in TRANSLATOR_TYPES) {
if(translator.translatorType & TRANSLATOR_TYPES[type]) {
_cache[type].push(translator);
}
}
} catch(e) {
Zotero.logError(e);
try {
Zotero.logError("Could not load translator "+JSON.stringify(translators[i]));
} catch(e) {}
}
}
// Sort by priority
var cmp = function (a, b) {
if (a.priority > b.priority) {
return 1;
}
else if (a.priority < b.priority) {
return -1;
}
}
for(var type in _cache) {
_cache[type].sort(cmp);
}
}
/**
* Gets the translator that corresponds to a given ID, without attempting to retrieve code
* @param {String} id The ID of the translator
*/
this.getWithoutCode = function(id) {
if(!_initialized) Zotero.Translators.init();
return _translators[id] ? _translators[id] : false;
}
/**
* Gets the translator that corresponds to a given ID
*
* @param {String} id The ID of the translator
*/
this.get = Zotero.Promise.method(function (id) {
if(!_initialized) Zotero.Translators.init();
var translator = _translators[id];
if(!translator) {
return false;
}
// only need to get code if it is of some use
if(translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER
&& !translator.hasOwnProperty("code")) {
return translator.getCode().then(() => translator);
} else {
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 {Boolean} [debugMode] Whether to assume debugging mode. If true, code is included for
* unsupported translators, and code originally retrieved from the
* repo is re-retrieved from Zotero Standalone.
*/
this.getAllForType = Zotero.Promise.method(function (type, debugMode) {
if(!_initialized) Zotero.Translators.init()
var translators = _cache[type].slice(0);
var codeGetter = new Zotero.Translators.CodeGetter(translators, debugMode);
return codeGetter.getAll().then(function() {
return translators;
});;
});
/**
* Gets web translators for a specific location
* @param {String} uri The URI for which to look for translators
* @return {Promise<Array[]>} - A promise for a 2-item array containing an array of translators and
* an array of functions for converting URLs from proper to proxied forms
*/
this.getWebTranslatorsForLocation = Zotero.Promise.method(function (uri) {
if(!_initialized) Zotero.Translators.init();
var allTranslators = _cache["web"];
var potentialTranslators = [];
var searchURIs = [uri];
Zotero.debug("Translators: Looking for translators for "+uri);
// if there is a subdomain that is also a TLD, also test against URI with the domain
// dropped after the TLD
// (i.e., www.nature.com.mutex.gmu.edu => www.nature.com)
var m = /^(https?:\/\/)([^\/]+)/i.exec(uri);
var properHosts = [];
var proxyHosts = [];
if(m) {
// 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("."));
}
}
}
var converterFunctions = [];
for(var i=0; i<allTranslators.length; i++) {
for(var j=0; j<searchURIs.length; j++) {
// don't attempt to use translators with no target that can't be run in this browser
// since that would require transmitting every page to Zotero host
if(!allTranslators[i].webRegexp
&& allTranslators[i].runMode !== Zotero.Translator.RUN_MODE_IN_BROWSER) {
continue;
}
if(!allTranslators[i].webRegexp
|| (uri.length < 8192 && allTranslators[i].webRegexp.test(searchURIs[j]))) {
// add translator to list
potentialTranslators.push(allTranslators[i]);
if(j === 0) {
converterFunctions.push(null);
} else if(Zotero.isBrowserExt || Zotero.isSafari) {
// in Chrome/Safari, the converterFunction needs to be passed as JSON, so
// just push an array with the proper and proxyHosts
converterFunctions.push([properHosts[j-1], proxyHosts[j-1]]);
} else {
// in Firefox, push the converterFunction
converterFunctions.push(new function() {
var re = new RegExp('^https?://(?:[^/]\\.)?'+Zotero.Utilities.quotemeta(properHosts[j-1])+'(?=/)', "gi");
var proxyHost = proxyHosts[j-1].replace(/\$/g, "$$$$");
return function(uri) { return uri.replace(re, "$&."+proxyHost) };
});
}
// don't add translator more than once
break;
}
}
}
var codeGetter = new Zotero.Translators.CodeGetter(potentialTranslators);
return codeGetter.getAll().then(function () {
return [potentialTranslators, converterFunctions];
});
});
/**
* Converts translators to JSON-serializable objects
*/
this.serialize = function(translator, properties) {
// handle translator arrays
if(translator.length !== undefined) {
var newTranslators = new Array(translator.length);
for(var i in translator) {
newTranslators[i] = Zotero.Translators.serialize(translator[i], properties);
}
return newTranslators;
}
// handle individual translator
var newTranslator = {};
for(var i in properties) {
var property = properties[i];
newTranslator[property] = translator[property];
}
return newTranslator;
}
/**
* Saves all translator metadata to localStorage
* @param {Object[]} newMetadata Metadata for new translators
* @param {Boolean} reset Whether to clear all existing translators and overwrite them with
* the specified translators.
*/
this.update = function(newMetadata, reset) {
if(!_initialized) Zotero.Translators.init();
if(!newMetadata.length) return;
if(reset) {
var serializedTranslators = newMetadata.filter(function(translator) {
return !translator.deleted;
});
} else {
var serializedTranslators = [];
var hasChanged = false;
// Update translators with new metadata
for(var i in newMetadata) {
var newTranslator = newMetadata[i];
if(newTranslator.deleted) {
// handle translator deletions
delete _translators[newTranslator.translatorID];
} else if(_translators.hasOwnProperty(newTranslator.translatorID)) {
var oldTranslator = _translators[newTranslator.translatorID];
// check whether translator has changed
if(oldTranslator.lastUpdated !== newTranslator.lastUpdated) {
// check whether newTranslator is actually newer than the existing
// translator, and if not, don't update
if(Zotero.Date.sqlToDate(newTranslator.lastUpdated) < Zotero.Date.sqlToDate(oldTranslator.lastUpdated)) {
continue;
}
Zotero.debug("Translators: Updating "+newTranslator.label);
oldTranslator.init(newTranslator);
hasChanged = true;
}
} else {
Zotero.debug("Translators: Adding "+newTranslator.label);
_translators[newTranslator.translatorID] = new Zotero.Translator(newTranslator);
hasChanged = true;
}
}
if(!hasChanged) return;
// Serialize translators
for(var i in _translators) {
var serializedTranslator = this.serialize(_translators[i], TRANSLATOR_SAVE_PROPERTIES);
// don't save run mode
delete serializedTranslator.runMode;
serializedTranslators.push(serializedTranslator);
}
}
// Store
if (Zotero.isBrowserExt || Zotero.isSafari) {
var serialized = JSON.stringify(serializedTranslators);
localStorage["translatorMetadata"] = serialized;
Zotero.debug("Translators: Saved updated translator list ("+serialized.length+" characters)");
}
// Reinitialize
Zotero.Translators.init(serializedTranslators);
}
/**
* Preprocesses code for a translator
*/
this.preprocessCode = function(code) {
if(!Zotero.isFx) {
const foreach = /^(\s*)for each\s*\((var )?([^ ]+) in (.*?)\)(\s*){/gm;
code = code.replace(foreach, "$1var $3_zForEachSubject = $4; "+
"for(var $3_zForEachIndex in $3_zForEachSubject)$5{ "+
"$2$3 = $3_zForEachSubject[$3_zForEachIndex];", code);
}
return code;
}
}
/**
* A class to get the code for a set of translators at once
*
* @param {Zotero.Translator[]} translators Translators for which to retrieve code
* @param {Boolean} [debugMode] If true, include code for unsupported translators
*/
Zotero.Translators.CodeGetter = function(translators, debugMode) {
this._translators = translators;
this._debugMode = debugMode;
this._concurrency = 1;
};
Zotero.Translators.CodeGetter.prototype.getCodeFor = Zotero.Promise.method(function(i) {
let translator = this._translators[i];
// retrieve code if no code and translator is supported locally
if((translator.runMode === Zotero.Translator.RUN_MODE_IN_BROWSER && !translator.hasOwnProperty("code"))
// or if debug mode is enabled (even if unsupported locally)
|| (this._debugMode && (!translator.hasOwnProperty("code")
// or if in debug mode and the code we have came from the repo (which doesn't
// include test cases)
|| (Zotero.Repo && translator.codeSource === Zotero.Repo.SOURCE_REPO)))) {
// get code
return translator.getCode();
}
});
Zotero.Translators.CodeGetter.prototype.getAll = function () {
var codes = [];
// Chain promises with some level of concurrency. If unchained, fires
// off hundreds of xhttprequests on connectors and crashes the extension
for (let i = 0; i < this._translators.length; i++) {
if (i < this._concurrency) {
codes.push(this.getCodeFor(i));
} else {
codes.push(codes[i-this._concurrency].then(() => this.getCodeFor(i)));
}
}
return Promise.all(codes);
};
var TRANSLATOR_REQUIRED_PROPERTIES = ["translatorID", "translatorType", "label", "creator", "target",
"priority", "lastUpdated"];
var TRANSLATOR_PASSING_PROPERTIES = TRANSLATOR_REQUIRED_PROPERTIES
.concat(["browserSupport", "code", "runMode", "itemType"]);
var TRANSLATOR_SAVE_PROPERTIES = TRANSLATOR_REQUIRED_PROPERTIES.concat(["browserSupport"]);
/**
* @class Represents an individual translator
* @constructor
* @property {String} translatorID Unique GUID of the translator
* @property {Integer} translatorType Type of the translator (use bitwise & with TRANSLATOR_TYPES to read)
* @property {String} label Human-readable name of the translator
* @property {String} creator Author(s) of the translator
* @property {String} target Location that the translator processes
* @property {String} minVersion Minimum Zotero version
* @property {String} maxVersion Minimum Zotero version
* @property {Integer} priority Lower-priority translators will be selected first
* @property {String} browserSupport String indicating browser supported by the translator
* g = Gecko (Firefox)
* c = Google Chrome (WebKit & V8)
* s = Safari (WebKit & Nitro/Squirrelfish Extreme)
* i = Internet Explorer
* b = Bookmarklet
* v = Server
* @property {Object} configOptions Configuration options for import/export
* @property {Object} displayOptions Display options for export
* @property {Object} hiddenPrefs Hidden preferences configurable through about:config
* @property {Boolean} inRepository Whether the translator may be found in the repository
* @property {String} lastUpdated SQL-style date and time of translator's last update
* @property {String} code The executable JavaScript for the translator
*/
Zotero.Translator = function(info) {
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(var i in TRANSLATOR_REQUIRED_PROPERTIES) {
var property = TRANSLATOR_REQUIRED_PROPERTIES[i];
if(info[property] === undefined) {
Zotero.logError(new Error('Missing property "'+property+'" in translator metadata JSON object in ' + info.label));
break;
} else {
this[property] = info[property];
}
}
this.browserSupport = info["browserSupport"] ? info["browserSupport"] : "g";
if(this.browserSupport.indexOf(Zotero.browser) !== -1) {
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
this.importRegexp = this.target ? new RegExp("\\."+this.target+"$", "i") : null;
} else if(this.hasOwnProperty("importRegexp")) {
delete this.importRegexp;
}
if(this.translatorType & TRANSLATOR_TYPES["web"]) {
// compile web regexp
this.webRegexp = this.target ? new RegExp(this.target, "i") : null;
} else if(this.hasOwnProperty("webRegexp")) {
delete this.webRegexp;
}
if(info.code) {
this.code = Zotero.Translators.preprocessCode(info.code);
} else if(this.hasOwnProperty("code")) {
delete this.code;
}
}
/**
* Retrieves code for this translator
*
* @return {Promise<String|false>} - Promise for translator code or false if none
*/
Zotero.Translator.prototype.getCode = function (debugMode) {
return Zotero.Repo.getTranslatorCode(this.translatorID, debugMode)
.then(function (args) {
var code = args[0];
var source = args[1];
if (!code) {
return false;
}
// cache code for session only (we have standalone anyway)
this.code = code;
this.codeSource = source;
return code;
}.bind(this));
}
/**
* Log a translator-related error
* @param {String} message The error message
* @param {String} [type] The error type ("error", "warning", "exception", or "strict")
* @param {String} [line] The text of the line on which the error occurred
* @param {Integer} lineNumber
* @param {Integer} colNumber
*/
Zotero.Translator.prototype.logError = function(message, type, line, lineNumber, colNumber) {
Zotero.logError(message);
}
Zotero.Translator.RUN_MODE_IN_BROWSER = 1;
Zotero.Translator.RUN_MODE_ZOTERO_STANDALONE = 2;
Zotero.Translator.RUN_MODE_ZOTERO_SERVER = 4;