578 lines
17 KiB
JavaScript
578 lines
17 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 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU 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 _ios;
|
|
var _timer;
|
|
|
|
/**
|
|
* Read locateEngines JSON file to initialize locate manager
|
|
*/
|
|
this.init = function() {
|
|
_ios = Components.classes["@mozilla.org/network/io-service;1"].
|
|
getService(Components.interfaces.nsIIOService);
|
|
|
|
_jsonFile = _getLocateFile();
|
|
|
|
if(_jsonFile.exists()) {
|
|
_locateEngines = [new LocateEngine(engine)
|
|
for each(engine in JSON.parse(Zotero.File.getContents(_jsonFile)))];
|
|
} else {
|
|
this.restoreDefaultEngines();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 "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() [new LocateEngine(engine)
|
|
for each(engine in JSON.parse(Zotero.File.getContents(_getDefaultFile())))];
|
|
|
|
/**
|
|
* Returns an array of all search engines
|
|
*/
|
|
this.getEngines = function() _locateEngines.slice(0);
|
|
|
|
/**
|
|
* Returns an array of all search engines visible that should be visible in the dropdown
|
|
*/
|
|
this.getVisibleEngines = function() [engine for each(engine in _locateEngines) if(!engine.hidden)];
|
|
|
|
/**
|
|
* Returns an engine with a specific name
|
|
*/
|
|
this.getEngineByName = function(engineName) {
|
|
engineName = engineName.toLowerCase();
|
|
for each(var engine in _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 each(var engine in _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 "Engine is not currently listed";
|
|
_locateEngines.splice(oldIndex, 1);
|
|
engine._removeIcon();
|
|
_serializeLocateEngines();
|
|
}
|
|
|
|
/**
|
|
* Restore default engines by copying file from extension dir
|
|
*/
|
|
this.restoreDefaultEngines = function() {
|
|
// get locate dir
|
|
var locateDir = _getLocateDirectory();
|
|
|
|
// remove old locate dir
|
|
if(locateDir.exists()) locateDir.remove(true);
|
|
|
|
// create new locate dir
|
|
locateDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0700);
|
|
|
|
// copy default file to new locate dir
|
|
_getDefaultFile().copyTo(locateDir, LOCATE_FILE_NAME);
|
|
|
|
// reread locate engines
|
|
this.init();
|
|
|
|
// reload icons for default locate engines
|
|
for each(var engine in this.getEngines()) engine._updateIcon();
|
|
}
|
|
|
|
/**
|
|
* Writes the engines to disk; called from the nsITimer spawned by _serializeLocateEngines
|
|
*/
|
|
this.notify = function() {
|
|
Zotero.File.putContents(_jsonFile, JSON.stringify(_locateEngines, null, "\t"));
|
|
_timer = undefined;
|
|
}
|
|
|
|
/**
|
|
* Gets the JSON file containing engine info
|
|
*/
|
|
function _getLocateFile() {
|
|
var locateDir = _getLocateDirectory();
|
|
locateDir.append(LOCATE_FILE_NAME);
|
|
return locateDir;
|
|
}
|
|
|
|
/**
|
|
* Gets the dir containing the JSON file and engine icons
|
|
*/
|
|
function _getLocateDirectory() {
|
|
var locateDir = Zotero.getZoteroDirectory();
|
|
locateDir.append(LOCATE_DIR_NAME);
|
|
return locateDir;
|
|
}
|
|
|
|
/**
|
|
* Gets the JSON file containing the engine info for the default engines
|
|
*/
|
|
function _getDefaultFile() {
|
|
var defaultFile = Zotero.getInstallDirectory();
|
|
defaultFile.append(LOCATE_FILE_NAME);
|
|
return defaultFile;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Called when an engine icon is downloaded to write it to disk
|
|
*/
|
|
function _engineIconLoaded(iconBytes, engine, contentType) {
|
|
const iconExtensions = {
|
|
"image/png":"png",
|
|
"image/jpeg":"jpg",
|
|
"image/gif":"gif",
|
|
"image/x-icon":"ico"
|
|
};
|
|
|
|
// ensure there is an icon
|
|
if(!iconBytes) throw "Icon could not be retrieved for "+engine.name;
|
|
|
|
// ensure there is an extension
|
|
var extension = iconExtensions[contentType.toLowerCase()];
|
|
if(!extension) throw "Invalid MIME type "+contentType+" for icon for engine "+engine.name;
|
|
|
|
// remove old icon
|
|
engine._removeIcon();
|
|
|
|
// find a good place to put the icon file
|
|
var sanitizedAlias = engine.name.replace(/[^\w _]/g, "");
|
|
var iconFile = _getLocateDirectory();
|
|
iconFile.append(sanitizedAlias + "." + extension);
|
|
if(iconFile.exists()) {
|
|
for(var i=0; iconFile.exists(); i++) {
|
|
iconFile = iconFile.parent;
|
|
iconFile.append(sanitizedAlias + "_" + i + "." + extension);
|
|
}
|
|
}
|
|
|
|
// write the icon to the file
|
|
var fos = Components.classes["@mozilla.org/network/file-output-stream;1"].
|
|
createInstance(Components.interfaces.nsIFileOutputStream);
|
|
fos.init(iconFile, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate
|
|
var bos = Components.classes["@mozilla.org/binaryoutputstream;1"].
|
|
createInstance(Components.interfaces.nsIBinaryOutputStream);
|
|
bos.setOutputStream(fos);
|
|
bos.writeByteArray(iconBytes, iconBytes.length);
|
|
bos.close();
|
|
|
|
// get the URI of the icon
|
|
engine.icon = _ios.newFileURI(iconFile).spec;
|
|
}
|
|
|
|
/**
|
|
* 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.getField("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.getField("title");
|
|
return (title ? [encodeURIComponent(title)] : false);
|
|
}
|
|
|
|
if(!itemOpenURL["rft."+param]) {
|
|
return false;
|
|
}
|
|
|
|
return [encodeURIComponent(val) for each(val in itemOpenURL["rft."+param])];
|
|
} else if(ns === "info:ofi/fmt:kev:mtx:ctx") {
|
|
if(!OPENURL_CONTEXT_MAPPINGS[param] || !itemOpenURL[OPENURL_CONTEXT_MAPPINGS[param]]) {
|
|
return false;
|
|
}
|
|
return [encodeURIComponent(val) for each(val in itemOpenURL[OPENURL_CONTEXT_MAPPINGS[param]])];
|
|
} else if(ns === "http://www.zotero.org/namespaces/openSearch#") {
|
|
if(param === "openURL") {
|
|
return [Zotero.OpenURL.createContextObject(item, "1.0")];
|
|
} else if(param === "year") {
|
|
return (itemOpenURL["rft.date"] ? [itemOpenURL["rft.date"][0].substr(0, 4)] : false);
|
|
} else {
|
|
var result = item.getField(param);
|
|
return (result ? [encodeURIComponent(result)] : false);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Theoretically implements nsISearchSubmission
|
|
*/
|
|
var LocateSubmission = function(uri, postData) {
|
|
this.uri = _ios.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 = this._urlTemplate = this.icon = null;
|
|
this.hidden = false;
|
|
this._urlParams = [];
|
|
|
|
if(obj) for(var prop in obj) this[prop] = obj[prop];
|
|
|
|
// Queue deferred serialization whenever a property is modified
|
|
for each(var prop in ["alias", "name", "description", "icon", "hidden"]) {
|
|
this.watch(prop, _watchLocateEngineProperties);
|
|
}
|
|
}
|
|
|
|
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 xml = Zotero.Styles.cleanXML(xmlStr);
|
|
if(OPENSEARCH_NAMESPACES.indexOf(xml.namespace()) === "-1") {
|
|
throw "Invalid namespace";
|
|
}
|
|
|
|
default xml namespace = xml.namespace();
|
|
|
|
// get simple attributes
|
|
this.alias = xml.ShortName.toString();
|
|
this.name = xml.LongName.toString();
|
|
if(!this.name) this.name = this.alias;
|
|
this.description = xml.Description.toString();
|
|
|
|
// get the URL template
|
|
this._urlTemplate = undefined;
|
|
for each(var urlTag in xml.Url.(@type.toLowerCase() == "text/html")) {
|
|
if(urlTag.@rel == undefined || urlTag.@rel == "results") {
|
|
this._urlTemplate = urlTag.@template.toString();
|
|
break;
|
|
}
|
|
this._method = urlTag.@method.toUpperCase() === "POST" ? "POST" : "GET";
|
|
}
|
|
|
|
// TODO: better error handling
|
|
if(!this._urlTemplate) throw "No URL found for required content type";
|
|
|
|
// get namespaces
|
|
this._urlNamespaces = {};
|
|
for each(var ns in urlTag.inScopeNamespaces()) {
|
|
this._urlNamespaces[ns.prefix] = ns.uri;
|
|
}
|
|
|
|
// get params
|
|
this._urlParams = [];
|
|
for each(var param in urlTag.Param) {
|
|
this._urlParams[param.@name.toString()] = param.@value.toString();
|
|
}
|
|
|
|
// find the icon
|
|
this._iconSourceURI = iconURL;
|
|
for each(var img in xml.Image) {
|
|
if((img.@width == undefined && img.@height == undefined)
|
|
|| (img.@width.toString() == "16" && img.@height.toString() == "16")) {
|
|
this._iconSourceURI = img.toString();
|
|
}
|
|
}
|
|
|
|
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 "LocateManager supports only responseType text/html";
|
|
}
|
|
|
|
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([encodeURIComponent(param)+"="+encodeURIComponent(val) for(val in result)]);
|
|
} 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 = _ios.newURI(this.icon, null, null);
|
|
var file = uri.QueryInterface(Components.interfaces.nsIFileURL).file;
|
|
if(file.exists()) file.remove(null);
|
|
},
|
|
|
|
"_updateIcon":function() {
|
|
// create new channel
|
|
var uri = _ios.newURI(this._iconSourceURI, null, null);
|
|
if(uri.scheme !== "http" && uri.scheme !== "https" && uri.scheme !== "ftp") return;
|
|
var chan = _ios.newChannelFromURI(uri);
|
|
var listener = new loadListener(chan, this, _engineIconLoaded);
|
|
chan.notificationCallbacks = listener;
|
|
chan.asyncOpen(listener, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ripped from nsSearchService.js
|
|
*/
|
|
function loadListener(aChannel, aEngine, aCallback) {
|
|
this._channel = aChannel;
|
|
this._bytes = [];
|
|
this._engine = aEngine;
|
|
this._callback = aCallback;
|
|
}
|
|
|
|
loadListener.prototype = {
|
|
_callback: null,
|
|
_channel: null,
|
|
_countRead: 0,
|
|
_engine: null,
|
|
_stream: null,
|
|
|
|
QueryInterface: function SRCH_loadQI(aIID) {
|
|
if (aIID.equals(Ci.nsISupports) ||
|
|
aIID.equals(Ci.nsIRequestObserver) ||
|
|
aIID.equals(Ci.nsIStreamListener) ||
|
|
aIID.equals(Ci.nsIChannelEventSink) ||
|
|
aIID.equals(Ci.nsIInterfaceRequestor) ||
|
|
aIID.equals(Ci.nsIBadCertListener2) ||
|
|
aIID.equals(Ci.nsISSLErrorListener) ||
|
|
// See FIXME comment below
|
|
aIID.equals(Ci.nsIHttpEventSink) ||
|
|
aIID.equals(Ci.nsIProgressEventSink) ||
|
|
false)
|
|
return this;
|
|
|
|
throw Cr.NS_ERROR_NO_INTERFACE;
|
|
},
|
|
|
|
// nsIRequestObserver
|
|
onStartRequest: function SRCH_loadStartR(aRequest, aContext) {
|
|
this._stream = Cc["@mozilla.org/binaryinputstream;1"].
|
|
createInstance(Ci.nsIBinaryInputStream);
|
|
},
|
|
|
|
onStopRequest: function SRCH_loadStopR(aRequest, aContext, aStatusCode) {
|
|
var requestFailed = !Components.isSuccessCode(aStatusCode);
|
|
if (!requestFailed && (aRequest instanceof Ci.nsIHttpChannel))
|
|
requestFailed = !aRequest.requestSucceeded;
|
|
|
|
if (requestFailed || this._countRead == 0) {
|
|
// send null so the callback can deal with the failure
|
|
this._callback(null, this._engine, this._channel.contentType);
|
|
} else
|
|
this._callback(this._bytes, this._engine, this._channel.contentType);
|
|
this._channel = null;
|
|
this._engine = null;
|
|
},
|
|
|
|
// nsIStreamListener
|
|
onDataAvailable: function SRCH_loadDAvailable(aRequest, aContext,
|
|
aInputStream, aOffset,
|
|
aCount) {
|
|
this._stream.setInputStream(aInputStream);
|
|
|
|
// Get a byte array of the data
|
|
this._bytes = this._bytes.concat(this._stream.readByteArray(aCount));
|
|
this._countRead += aCount;
|
|
},
|
|
|
|
// nsIChannelEventSink
|
|
onChannelRedirect: function SRCH_loadCRedirect(aOldChannel, aNewChannel,
|
|
aFlags) {
|
|
this._channel = aNewChannel;
|
|
},
|
|
|
|
// nsIInterfaceRequestor
|
|
getInterface: function SRCH_load_GI(aIID) {
|
|
return this.QueryInterface(aIID);
|
|
},
|
|
|
|
// nsIBadCertListener2
|
|
notifyCertProblem: function SRCH_certProblem(socketInfo, status, targetSite) {
|
|
return true;
|
|
},
|
|
|
|
// nsISSLErrorListener
|
|
notifySSLError: function SRCH_SSLError(socketInfo, error, targetSite) {
|
|
return true;
|
|
},
|
|
|
|
// FIXME: bug 253127
|
|
// nsIHttpEventSink
|
|
onRedirect: function (aChannel, aNewChannel) {},
|
|
// nsIProgressEventSink
|
|
onProgress: function (aRequest, aContext, aProgress, aProgressMax) {},
|
|
onStatus: function (aRequest, aContext, aStatus, aStatusArg) {}
|
|
}
|
|
} |