503 lines
15 KiB
JavaScript
503 lines
15 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 *****
|
|
*/
|
|
|
|
Zotero.LocateManager = new function() {
|
|
const LOCATE_FILE_NAME = "engines.json";
|
|
const LOCATE_DIR_NAME = "locate";
|
|
|
|
var _jsonFile;
|
|
var _locateEngines;
|
|
var _timer;
|
|
|
|
/**
|
|
* Read locateEngines JSON file to initialize locate manager
|
|
*/
|
|
this.init = async function() {
|
|
_jsonFile = _getLocateFile();
|
|
|
|
try {
|
|
if (await OS.File.exists(_jsonFile)) {
|
|
_locateEngines = JSON.parse(await Zotero.File.getContentsAsync(_jsonFile))
|
|
.map(engine => new LocateEngine(engine));
|
|
}
|
|
else {
|
|
await this.restoreDefaultEngines();
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a new search engine
|
|
* confirm parameter is currently ignored
|
|
*/
|
|
this.addEngine = function(engineURL, dataType, iconURL, confirm) {
|
|
if(dataType !== Components.interfaces.nsISearchEngine.TYPE_OPENSEARCH) {
|
|
throw new Error("LocateManager supports only OpenSearch engines");
|
|
}
|
|
|
|
Zotero.HTTP.doGet(engineURL, function(xmlhttp) {
|
|
var engine = new LocateEngine();
|
|
engine.initWithXML(xmlhttp.responseText, iconURL);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Gets all default search engines (not currently used)
|
|
*/
|
|
this.getDefaultEngines = function () {
|
|
return JSON.parse(Zotero.File.getContentsFromURL(_getDefaultFile()))
|
|
.map(engine => new LocateEngine(engine));
|
|
}
|
|
|
|
/**
|
|
* Returns an array of all search engines
|
|
*/
|
|
this.getEngines = function() { return _locateEngines.slice(0); }
|
|
|
|
/**
|
|
* Returns an array of all search engines visible that should be visible in the dropdown
|
|
*/
|
|
this.getVisibleEngines = function () {
|
|
return _locateEngines.filter(engine => !engine.hidden);
|
|
}
|
|
|
|
/**
|
|
* Returns an engine with a specific name
|
|
*/
|
|
this.getEngineByName = function(engineName) {
|
|
engineName = engineName.toLowerCase();
|
|
for (let engine of _locateEngines) if(engine.name.toLowerCase() == engineName) return engine;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the first engine with a specific alias
|
|
*/
|
|
this.getEngineByAlias = function(engineAlias) {
|
|
engineAlias = engineAlias.toLowerCase();
|
|
for (let engine of _locateEngines) if(engine.alias.toLowerCase() == engineAlias) return engine;
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Moves an engine in the list
|
|
*/
|
|
this.moveEngine = function(engine, newIndex) {
|
|
this.removeEngine(engine);
|
|
_locateEngines.splice(newIndex, engine);
|
|
}
|
|
|
|
/**
|
|
* Removes an engine from the list
|
|
*/
|
|
this.removeEngine = function(engine) {
|
|
var oldIndex = _locateEngines.indexOf(engine);
|
|
if(oldIndex === -1) throw new Error("Engine is not currently listed");
|
|
_locateEngines.splice(oldIndex, 1);
|
|
engine._removeIcon();
|
|
_serializeLocateEngines();
|
|
}
|
|
|
|
/**
|
|
* Restore default engines by copying file from extension dir
|
|
*/
|
|
this.restoreDefaultEngines = async function () {
|
|
// get locate dir
|
|
var locateDir = _getLocateDirectory();
|
|
|
|
// remove old locate dir
|
|
await OS.File.removeDir(locateDir, { ignoreAbsent: true, ignorePermissions: true });
|
|
|
|
// create new locate dir
|
|
await OS.File.makeDir(locateDir, { unixMode: 0o755 });
|
|
|
|
// copy default file to new locate dir
|
|
await Zotero.File.putContentsAsync(
|
|
_jsonFile,
|
|
await Zotero.File.getContentsFromURLAsync(_getDefaultFile())
|
|
);
|
|
|
|
// reread locate engines
|
|
await this.init();
|
|
|
|
// reload icons for default locate engines
|
|
for (let engine of this.getEngines()) engine._updateIcon();
|
|
}
|
|
|
|
/**
|
|
* Writes the engines to disk; called from the nsITimer spawned by _serializeLocateEngines
|
|
*/
|
|
this.notify = async function () {
|
|
await Zotero.File.putContentsAsync(_jsonFile, JSON.stringify(_locateEngines, null, "\t"));
|
|
_timer = undefined;
|
|
}
|
|
|
|
/**
|
|
* Gets the JSON file containing engine info
|
|
*/
|
|
function _getLocateFile() {
|
|
return OS.Path.join(_getLocateDirectory(), LOCATE_FILE_NAME);
|
|
}
|
|
|
|
/**
|
|
* Gets the dir containing the JSON file and engine icons
|
|
*/
|
|
function _getLocateDirectory() {
|
|
return OS.Path.join(Zotero.DataDirectory.dir, LOCATE_DIR_NAME);
|
|
}
|
|
|
|
/**
|
|
* Gets the JSON file containing the engine info for the default engines
|
|
*/
|
|
function _getDefaultFile() {
|
|
return "resource://zotero/schema/"+LOCATE_FILE_NAME;
|
|
}
|
|
|
|
|
|
/**
|
|
* Writes the engines to disk when the current block is finished executing
|
|
*/
|
|
function _serializeLocateEngines() {
|
|
if(_timer) return;
|
|
_timer = Components.classes["@mozilla.org/timer;1"].
|
|
createInstance(Components.interfaces.nsITimer);
|
|
_timer.initWithCallback(Zotero.LocateManager, 0, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
|
|
/**
|
|
* Function to call to attach to watch engine properties and perform deferred serialization
|
|
*/
|
|
function _watchLocateEngineProperties(id, oldval, newval) {
|
|
if(oldval !== newval) _serializeLocateEngines();
|
|
return newval;
|
|
}
|
|
|
|
/**
|
|
* Looks up a parameter in our list
|
|
*
|
|
* Supported parameters include
|
|
* - all standard OpenURL parameters, identified by any OpenURL namespace
|
|
* - "version", "identifier", and "format" identified by the OpenURL ctx namespace
|
|
* - "openURL" identified by the Zotero namespace (= the whole openURL)
|
|
* - "year" identified by the Zotero namespace
|
|
* - any Zotero field identified by the Zotero namespace
|
|
*/
|
|
function _lookupParam(item, itemOpenURL, engine, nsPrefix, param) {
|
|
const OPENURL_ITEM_PREFIXES = [
|
|
"info:ofi/fmt:kev:mtx:journal",
|
|
"info:ofi/fmt:kev:mtx:book",
|
|
"info:ofi/fmt:kev:mtx:patent",
|
|
"info:ofi/fmt:kev:mtx:sch_svc",
|
|
"info:ofi/fmt:kev:mtx:dissertation"
|
|
];
|
|
|
|
const OPENURL_CONTEXT_MAPPINGS = {
|
|
"version":"ctx_ver",
|
|
"identifier":"rfr_id",
|
|
"format":"rft_val_fmt"
|
|
};
|
|
|
|
if(nsPrefix) {
|
|
var ns = engine._urlNamespaces[nsPrefix];
|
|
if(!ns) return false;
|
|
} else {
|
|
if(param === "searchTerms") return [item.title];
|
|
return false;
|
|
}
|
|
|
|
if(OPENURL_ITEM_PREFIXES.indexOf(ns) !== -1) {
|
|
// take a normal "title," even though we don't use it, because it is valid (but not
|
|
// preferred) OpenURL
|
|
if(param === "title") {
|
|
var title = item.title;
|
|
return (title ? [encodeURIComponent(title)] : false);
|
|
}
|
|
|
|
if(!itemOpenURL["rft."+param]) {
|
|
return false;
|
|
}
|
|
|
|
return itemOpenURL["rft."+param].map(val => encodeURIComponent(val));
|
|
} else if(ns === "info:ofi/fmt:kev:mtx:ctx") {
|
|
if(!OPENURL_CONTEXT_MAPPINGS[param] || !itemOpenURL[OPENURL_CONTEXT_MAPPINGS[param]]) {
|
|
return false;
|
|
}
|
|
return itemOpenURL[OPENURL_CONTEXT_MAPPINGS[param]].map(val => encodeURIComponent(val));
|
|
} else if(ns === "http://www.zotero.org/namespaces/openSearch#") {
|
|
if(param === "openURL") {
|
|
var ctx = Zotero.OpenURL.createContextObject(item, "1.0");
|
|
return (ctx ? [ctx] : false);
|
|
} else if(param === "year") {
|
|
return (itemOpenURL["rft.date"] ? [itemOpenURL["rft.date"][0].substr(0, 4)] : false);
|
|
} else {
|
|
var result = item[param];
|
|
return (result ? [encodeURIComponent(result)] : false);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Theoretically implements nsISearchSubmission
|
|
*/
|
|
var LocateSubmission = function(uri, postData) {
|
|
this.uri = Services.io.newURI(uri, null, null);
|
|
this.postData = postData;
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* Constructs a new LocateEngine
|
|
* @param {Object} [obj] The locate engine, in parsed form, as it was serialized to JSON
|
|
*/
|
|
var LocateEngine = function(obj) {
|
|
this._alias = this._name = "Untitled";
|
|
this._description = null;
|
|
this._icon = null;
|
|
this._hidden = false;
|
|
this._urlTemplate = null;
|
|
this._urlParams = [];
|
|
|
|
if(obj) for(var prop in obj) this[prop] = obj[prop];
|
|
}
|
|
|
|
LocateEngine.prototype = {
|
|
/**
|
|
* Initializes an engine with a string and an iconURL to use if none is defined in the file
|
|
*/
|
|
"initWithXML":function(xmlStr, iconURL) {
|
|
const OPENSEARCH_NAMESPACES = [
|
|
// These are the official namespaces
|
|
"http://a9.com/-/spec/opensearch/1.1/",
|
|
"http://a9.com/-/spec/opensearch/1.0/",
|
|
// These were also in nsSearchService.js
|
|
"http://a9.com/-/spec/opensearchdescription/1.1/",
|
|
"http://a9.com/-/spec/opensearchdescription/1.0/"
|
|
];
|
|
|
|
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
|
.createInstance(Components.interfaces.nsIDOMParser),
|
|
doc = parser.parseFromString(xmlStr, "application/xml"),
|
|
docEl = doc.documentElement,
|
|
ns = docEl.namespaceURI,
|
|
xns = {"s":doc.documentElement.namespaceURI,
|
|
"xmlns":"http://www.w3.org/2000/xmlns"};
|
|
if(OPENSEARCH_NAMESPACES.indexOf(ns) === -1) {
|
|
throw new Error("Invalid namespace");
|
|
}
|
|
|
|
// get simple attributes
|
|
this.alias = Zotero.Utilities.xpathText(docEl, 's:ShortName', xns);
|
|
this.name = Zotero.Utilities.xpathText(docEl, 's:LongName', xns);
|
|
if(!this.name) this.name = this.alias;
|
|
this.description = Zotero.Utilities.xpathText(docEl, 's:Description', xns);
|
|
|
|
// get the URL template
|
|
this._urlTemplate = undefined;
|
|
var urlTags = Zotero.Utilities.xpath(docEl, 's:Url[@type="text/html"]', xns),
|
|
i = 0;
|
|
while(urlTags[i].hasAttribute("rel") && urlTags[i].getAttribute("rel") != "results") {
|
|
i++;
|
|
if(i == urlTags.length) throw new Error("No Url tag found");
|
|
}
|
|
|
|
// TODO: better error handling
|
|
var urlTag = urlTags[i];
|
|
this._urlTemplate = urlTag.getAttribute("template")
|
|
this._method = urlTag.getAttribute("method").toString().toUpperCase() === "POST" ? "POST" : "GET";
|
|
|
|
// get namespaces
|
|
this._urlNamespaces = {};
|
|
var node = urlTag;
|
|
while(node && node.attributes) {
|
|
for(var i=0; i<node.attributes.length; i++) {
|
|
var attr = node.attributes[i];
|
|
if(attr.namespaceURI == "http://www.w3.org/2000/xmlns/") {
|
|
this._urlNamespaces[attr.localName] = attr.nodeValue;
|
|
}
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
|
|
// get params
|
|
this._urlParams = [];
|
|
for(var param of Zotero.Utilities.xpath(urlTag, 's:Param', xns)) {
|
|
this._urlParams[param.getAttribute("name")] = param.getAttribute("value");
|
|
}
|
|
|
|
// find the icon
|
|
this._iconSourceURI = iconURL;
|
|
for(var img of Zotero.Utilities.xpath(docEl, 's:Image', xns)) {
|
|
if((!img.hasAttribute("width") && !img.hasAttribute("height"))
|
|
|| (img.getAttribute("width") == "16" && img.getAttribute("height") == "16")) {
|
|
this._iconSourceURI = img.textContent;
|
|
}
|
|
}
|
|
|
|
if(this._iconSourceURI) {
|
|
// begin fetching the icon if necesssary
|
|
this._updateIcon();
|
|
}
|
|
|
|
// delete any old engine with the same name
|
|
var engine = Zotero.LocateManager.getEngineByName(this.name);
|
|
if(engine) Zotero.LocateManager.removeEngine(engine);
|
|
|
|
// add and serialize the new engine
|
|
_locateEngines.push(this);
|
|
_serializeLocateEngines();
|
|
},
|
|
|
|
"getItemSubmission":function(item, responseType) {
|
|
if(responseType && responseType !== "text/html") {
|
|
throw new Error("LocateManager supports only responseType text/html");
|
|
}
|
|
|
|
if (item.toJSON) {
|
|
item = item.toJSON();
|
|
}
|
|
|
|
var itemAsOpenURL = Zotero.OpenURL.createContextObject(item, "1.0", true);
|
|
|
|
// do substitutions
|
|
var me = this;
|
|
var abort = false;
|
|
var url = this._urlTemplate.replace(/{(?:([^}:]+):)?([^}:?]+)(\?)?}/g, function(all, nsPrefix, param, required) {
|
|
var result = _lookupParam(item, itemAsOpenURL, me, nsPrefix, param, required);
|
|
if(result) {
|
|
return result[0];
|
|
} else {
|
|
if(required) { // if no param and it wasn't optional, return
|
|
return "";
|
|
} else {
|
|
abort = true;
|
|
}
|
|
}
|
|
});
|
|
if(abort) return null;
|
|
|
|
// handle params
|
|
var paramsToAdd = [];
|
|
for(var param in this._urlParams) {
|
|
var m = this._urlParams[param].match(/^{(?:([^}:]+):)?([^}:?]+)(\?)?}$/);
|
|
if(!m) {
|
|
paramsToAdd.push(encodeURIComponent(param)+"="+encodeURIComponent(this._urlParams[param]));
|
|
} else {
|
|
var result = _lookupParam(item, itemAsOpenURL, me, m[1], m[2]);
|
|
if(result) {
|
|
paramsToAdd = paramsToAdd.concat(
|
|
result.map(val =>
|
|
encodeURIComponent(param) + "=" + encodeURIComponent(val))
|
|
);
|
|
} else if(m[3]) { // if no param and it wasn't optional, return
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// attach params
|
|
if(paramsToAdd.length) {
|
|
if(this._method === "POST") {
|
|
var postData = paramsToAdd.join("&");
|
|
} else {
|
|
var postData = null;
|
|
if(url.indexOf("?") === -1) {
|
|
url += "?"+paramsToAdd.join("&");
|
|
} else {
|
|
url += "&"+paramsToAdd.join("&");
|
|
}
|
|
}
|
|
}
|
|
|
|
return new LocateSubmission(url, postData);
|
|
},
|
|
|
|
"_removeIcon":function() {
|
|
if(!this.icon) return;
|
|
var uri = Services.io.newURI(this.icon, null, null);
|
|
var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
|
|
if(file.exists()) file.remove(null);
|
|
},
|
|
|
|
_updateIcon: async function () {
|
|
const iconExtensions = {
|
|
"image/png": "png",
|
|
"image/jpeg": "jpg",
|
|
"image/gif": "gif",
|
|
"image/vnd.microsoft.icon": "ico"
|
|
};
|
|
|
|
if (!this._iconSourceURI.startsWith('http') && !this._iconSourceURI.startsWith('https')) {
|
|
return;
|
|
}
|
|
|
|
var tmpPath = OS.Path.join(Zotero.getTempDirectory().path, Zotero.Utilities.randomString());
|
|
await Zotero.File.download(this._iconSourceURI, tmpPath);
|
|
|
|
var sample = await Zotero.File.getSample(tmpPath);
|
|
var contentType = Zotero.MIME.getMIMETypeFromData(sample);
|
|
|
|
// ensure there is an extension
|
|
var extension = iconExtensions[contentType.toLowerCase()];
|
|
if (!extension) {
|
|
throw new Error(`Invalid content type ${contentType} for icon for engine ${this.name}`);
|
|
}
|
|
|
|
// Find a good place to put the icon file
|
|
var sanitizedAlias = this.name.replace(/[^\w _]/g, "");
|
|
var iconFile = OS.Path.join(_getLocateDirectory(), sanitizedAlias + "." + extension);
|
|
if (await OS.File.exists(iconFile)) {
|
|
for (let i = 0; await OS.File.exists(iconFile); i++) {
|
|
iconFile = OS.Path.join(
|
|
OS.Path.dirname(iconFile),
|
|
sanitizedAlias + "_" + i + "." + extension
|
|
);
|
|
}
|
|
}
|
|
|
|
await OS.File.move(tmpPath, iconFile);
|
|
this.icon = OS.Path.toFileURI(iconFile);
|
|
}
|
|
}
|
|
|
|
// Queue deferred serialization whenever a property is modified
|
|
for (let prop of ["alias", "name", "description", "icon", "hidden"]) {
|
|
let propName = '_' + prop;
|
|
Object.defineProperty(LocateEngine.prototype, prop, {
|
|
get: function () {
|
|
return this[propName];
|
|
},
|
|
set: function (val) {
|
|
var oldVal = this[propName];
|
|
this[propName] = val;
|
|
_watchLocateEngineProperties(prop, oldVal, val);
|
|
}
|
|
});
|
|
}
|
|
}
|