zotero/chrome/content/zotero/xpcom/translation/translators.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

472 lines
14 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 *****
*/
"use strict";
// 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 translator metadata into memory
*
* @param {Object} [options.metadataCache] - Translator metadata keyed by filename, if already
* available (e.g., in updateBundledFiles()), to avoid unnecesary file reads
*/
this.reinit = Zotero.Promise.coroutine(function* (options = {}) {
Zotero.debug("Initializing translators");
var start = new Date;
_cache = {"import":[], "export":[], "web":[], "search":[]};
_translators = {};
var sql = "SELECT fileName, metadataJSON, lastModifiedTime FROM translatorCache";
var dbCacheResults = yield Zotero.DB.queryAsync(sql);
var dbCache = {};
for (let i = 0; i < dbCacheResults.length; i++) {
let entry = dbCacheResults[i];
dbCache[entry.fileName] = entry;
}
var numCached = 0;
var filesInCache = {};
var translatorsDir = Zotero.getTranslatorsDirectory().path;
var iterator = new OS.File.DirectoryIterator(translatorsDir);
try {
while (true) {
let entries = yield iterator.nextBatch(5); // TODO: adjust as necessary
if (!entries.length) break;
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
let path = entry.path;
let fileName = entry.name;
if (!(/^[^.].*\.js$/.test(fileName))) continue;
let lastModifiedTime;
if ('winLastWriteDate' in entry) {
lastModifiedTime = entry.winLastWriteDate.getTime();
}
else {
lastModifiedTime = (yield OS.File.stat(path)).lastModificationDate.getTime();
}
// Check passed cache for metadata
let memCacheJSON = false;
if (options.metadataCache && options.metadataCache[fileName]) {
memCacheJSON = options.metadataCache[fileName];
}
// Check DB cache
let dbCacheEntry = false;
if (dbCache[fileName]) {
filesInCache[fileName] = true;
if (dbCache[fileName].lastModifiedTime == lastModifiedTime) {
dbCacheEntry = dbCache[fileName];
}
}
// Get JSON from cache if possible
if (memCacheJSON || dbCacheEntry) {
var translator = Zotero.Translators.load(
memCacheJSON || dbCacheEntry.metadataJSON, path
);
}
// Otherwise, load from file
else {
try {
var translator = yield Zotero.Translators.loadFromFile(path);
}
catch (e) {
Zotero.logError(e);
// If translator file is invalid, delete it and clear the cache entry
// so that the translator is reinstalled the next time it's updated.
//
// TODO: Reinstall the correct translator immediately
yield OS.File.remove(path);
let sql = "DELETE FROM translatorCache WHERE fileName=?";
yield Zotero.DB.queryAsync(sql, fileName);
continue;
}
}
// When can this happen?
if (!translator.translatorID) {
Zotero.debug("Translator ID for " + path + " not found");
continue;
}
// Check if there's already a cached translator with the same id
if (_translators[translator.translatorID]) {
let existingTranslator = _translators[translator.translatorID];
// If cached translator is older, delete it
if (existingTranslator.lastUpdated < translator.lastUpdated) {
translator.logError("Deleting older translator "
+ existingTranslator.fileName + " with same ID as "
+ translator.fileName);
yield OS.File.remove(existingTranslator.path);
delete _translators[translator.translatorID];
}
// If cached translator is newer or the same, delete the current one
else {
translator.logError("Translator " + existingTranslator.fileName
+ " with same ID is already loaded -- deleting "
+ translator.fileName);
yield OS.File.remove(translator.path);
continue;
}
}
// add to cache
_translators[translator.translatorID] = translator;
for (let type in TRANSLATOR_TYPES) {
if (translator.translatorType & TRANSLATOR_TYPES[type]) {
_cache[type].push(translator);
}
}
if (!dbCacheEntry) {
yield Zotero.Translators.cacheInDB(
fileName,
translator.serialize(TRANSLATOR_REQUIRED_PROPERTIES.
concat(TRANSLATOR_OPTIONAL_PROPERTIES)),
lastModifiedTime
);
}
numCached++;
}
}
}
finally {
iterator.close();
}
// Remove translators from DB cache if no file
for (let fileName in dbCache) {
if (!filesInCache[fileName]) {
yield Zotero.DB.queryAsync(
"DELETE FROM translatorCache WHERE fileName = ?", fileName
);
}
}
// 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);
}
_initialized = true;
Zotero.debug("Cached " + numCached + " translators in " + ((new Date) - start) + " ms");
});
this.init = Zotero.lazy(this.reinit);
/**
* Loads a translator from JSON, with optional code
*
* @param {String|Object} json - Metadata JSON
* @param {String} path
* @param {String} [code]
*/
this.load = function (json, path, code) {
var info = typeof json == 'string' ? JSON.parse(json) : json;
info.path = path;
info.code = code;
return new Zotero.Translator(info);
}
/**
* Loads a translator from the disk
*
* @param {String} file - Path to translator file
*/
this.loadFromFile = function(path) {
const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/;
return Zotero.File.getContentsAsync(path)
.then(function(source) {
return Zotero.Translators.load(infoRe.exec(source)[0], path, source);
})
.catch(function() {
throw "Invalid or missing translator metadata JSON object in " + OS.Path.basename(path);
});
}
/**
* Gets the translator that corresponds to a given ID
*
* @param {String} id The ID of the translator
*/
this.get = function(id) {
if (!_initialized) {
throw new Zotero.Exception.UnloadedDataException("Translators not yet loaded", 'translators');
}
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)
*/
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 Object.keys(_translators).map(id => _translators[id]);
});
}
/**
* Gets web translators for a specific location
* @param {String} uri The URI for which to look for translators
*/
this.getWebTranslatorsForLocation = function(uri) {
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
* @return {Promise<Zotero.Translator[]|true>} - An array of translators if no callback is specified;
* otherwise true
*/
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 = Zotero.Promise.coroutine(function* (metadata, code) {
if (!metadata.translatorID) {
throw ("metadata.translatorID not provided in Zotero.Translators.save()");
}
if (!metadata.translatorType) {
var found = false;
for (let type in TRANSLATOR_TYPES) {
if (metadata.translatorType & TRANSLATOR_TYPES[type]) {
found = true;
break;
}
}
if (!found) {
throw ("Invalid translatorType '" + metadata.translatorType + "' in Zotero.Translators.save()");
}
}
if (!metadata.label) {
throw new Error("metadata.label not provided");
}
if (!metadata.priority) {
throw new Error("metadata.priority not provided");
}
if (!metadata.lastUpdated) {
throw new Error("metadata.lastUpdated not provided");
}
if (!code) {
throw new Error("code not provided");
}
var fileName = Zotero.Translators.getFileNameFromLabel(
metadata.label, metadata.translatorID
);
var destFile = OS.Path.join(Zotero.getTranslatorsDirectory().path, fileName);
// JSON.stringify has the benefit of indenting JSON
var metadataJSON = JSON.stringify(metadata, null, "\t");
var str = metadataJSON + "\n\n" + code,
translator;
var translator = Zotero.Translators.get(metadata.translatorID);
var sameFile = translator && destFile == translator.path;
var exists = yield OS.File.exists(destFile);
if (!sameFile && exists) {
var msg = "Overwriting translator with same filename '"
+ fileName + "'";
Zotero.debug(msg, 1);
Zotero.debug(metadata, 1);
Components.utils.reportError(msg);
}
Zotero.debug("Saving translator '" + metadata.label + "'");
Zotero.debug(metadata);
return Zotero.File.putContentsAsync(destFile, str).return(destFile);
});
this.cacheInDB = function(fileName, metadataJSON, lastModifiedTime) {
return Zotero.DB.queryAsync(
"REPLACE INTO translatorCache VALUES (?, ?, ?)",
[fileName, JSON.stringify(metadataJSON), lastModifiedTime]
);
}
}