Proxy system overhaul

- Only one preference for recognizing proxies for transparent redirection
- Blacklists sites and http://%h/%p
- Fixes to EZProxy

This will clear all existing proxies from Zotero
This commit is contained in:
Simon Kornblith 2009-03-23 19:55:57 +00:00
parent 5aae6bc9ce
commit 9ca461c59b
9 changed files with 251 additions and 175 deletions

View file

@ -24,7 +24,7 @@ var openURLServerField;
var openURLVersionMenu;
var proxies;
var charsets;
var _io;
var _io = {};
function init()
{
@ -47,6 +47,7 @@ function init()
charsetMap[Zotero.Prefs.get("import.charset")] ?
charsetMap[Zotero.Prefs.get("import.charset")] : charsetMap["auto"];
if(window.arguments) {
_io = window.arguments[0];
if(_io.pane) {
@ -58,6 +59,7 @@ function init()
}
}
}
}
function onDataDirLoad() {
@ -1313,7 +1315,13 @@ function deleteProxy() {
*/
function refreshProxyList() {
// get and sort proxies
proxies = Zotero.Proxies.get();
proxies = Zotero.Proxies.proxies.slice();
for(var i=0; i<proxies.length; i++) {
if(!proxies[i].proxyID) {
proxies.splice(i, 1);
i--;
}
}
proxies = proxies.sort(function(a, b) {
if(a.multiHost) {
if(b.multiHost) {
@ -1362,7 +1370,6 @@ function refreshProxyList() {
document.getElementById('proxyTree').currentIndex = -1;
document.getElementById('proxyTree-delete').disabled = true;
document.getElementById('zotero-proxies-autoRecognize').checked = Zotero.Prefs.get("proxies.autoRecognize");
document.getElementById('zotero-proxies-transparent').checked = Zotero.Prefs.get("proxies.transparent");
}
@ -1370,16 +1377,10 @@ function refreshProxyList() {
* Updates proxy autoRecognize and transparent settings based on checkboxes
*/
function updateProxyPrefs() {
Zotero.Prefs.set("proxies.autoRecognize", document.getElementById('zotero-proxies-autoRecognize').checked);
Zotero.Prefs.set("proxies.transparent", document.getElementById('zotero-proxies-transparent').checked);
var oldTransparent = Zotero.Prefs.get("proxies.transparent");
var newTransparent = document.getElementById('zotero-proxies-transparent').checked;
if(!oldTransparent && newTransparent) {
Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService).alert(window,
Zotero.getString("proxies.enableTransparentWarning.title"),
Zotero.getString("proxies.enableTransparentWarning.description"));
}
Zotero.Prefs.set("proxies.transparent", newTransparent);
Zotero.Proxies.init()

View file

@ -535,13 +535,16 @@ To add a new preference:
<prefpane id="zotero-prefpane-proxies"
label="&zotero.preferences.prefpane.proxies;"
image="chrome://zotero/skin/prefs-proxies.png">
<groupbox>
<description width="45em" style="font-size: 12px">
&zotero.preferences.proxies.desc_before_link;
<label class="text-link" href="http://www.zotero.org/support/proxies"
value="&zotero.preferences.proxies.desc_link;"/>
&zotero.preferences.proxies.desc_after_link;
</description>
<command id="zotero-proxies-update" oncommand="updateProxyPrefs()"/>
<checkbox id="zotero-proxies-autoRecognize" label="&zotero.preferences.proxies.autoRecognize;"
command="zotero-proxies-update"/>
<checkbox id="zotero-proxies-transparent" label="&zotero.preferences.proxies.transparent;"
command="zotero-proxies-update"/>
</groupbox>
<groupbox flex="1">
<caption label="&zotero.preferences.proxies.configured;"/>

View file

@ -111,17 +111,18 @@ var Zotero_ProxyEditor = new function() {
var hasErrors = proxy.validate();
if(hasErrors) {
error = hasErrors.shift();
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
promptService.alert(
window,
Zotero.getString("proxies.error"),
Zotero.getString("proxies.error." + hasErrors)
window, Zotero.getString("proxies.error"),
Zotero.getString("proxies.error." + error, hasErrors)
);
if(window.arguments && window.arguments[0]) proxy.revert();
return false;
}
proxy.save();
proxy.save(true);
return true;
}

View file

@ -17,25 +17,23 @@
See the License for the specific language governing permissions and
limitations under the License.
Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed)
***** END LICENSE BLOCK *****
*/
/**
* A singleton to handle URL rewriting proxies
* @namespace
* @property transparent {Boolean} Whether transparent proxy functionality is enabled
* @property proxies {Zotero.Proxy[]} All loaded proxies
* @property hosts {Zotero.Proxy{}} Object mapping hosts to proxies
*/
Zotero.Proxies = new function() {
var on = false;
var proxies = false;
var hosts;
this.proxies = false;
this.transparent = false;
this.hosts = {};
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
var autoRecognize = false;
var transparent = false;
var lastRecognizedURI = false;
var lastButton = false;
@ -43,15 +41,21 @@ Zotero.Proxies = new function() {
* Initializes http-on-examine-response observer to intercept page loads and gets preferences
*/
this.init = function() {
if(!on) {
if(!this.proxies) {
var me = this;
Zotero.MIMETypeHandler.addObserver(function(ch) { me.observe(ch) });
this.get();
on = true;
var rows = Zotero.DB.query("SELECT * FROM proxies");
Zotero.Proxies.proxies = [new Zotero.Proxy(row) for each(row in rows)];
for each(var proxy in Zotero.Proxies.proxies) {
for each(var host in proxy.hosts) {
Zotero.Proxies.hosts[host] = proxy;
}
}
}
autoRecognize = Zotero.Prefs.get("proxies.autoRecognize");
transparent = Zotero.Prefs.get("proxies.transparent");
Zotero.Proxies.transparent = Zotero.Prefs.get("proxies.transparent");
}
/**
@ -69,23 +73,22 @@ Zotero.Proxies = new function() {
// see if there is a proxy we already know
var m = false;
var proxy;
for each(proxy in proxies) {
if(proxy.regexp && proxy.multiHost) {
for each(proxy in Zotero.Proxies.proxies) {
if(proxy.proxyID && proxy.regexp && proxy.multiHost) {
m = proxy.regexp.exec(url);
if(m) break;
}
}
if(m) {
// add this host if we know a proxy
if(proxy.autoAssociate) {
var host = m[proxy.parameters.indexOf("%h")+1];
if(proxy.hosts.indexOf(host) == -1) {
// add this host if we know a proxy
if(proxy.autoAssociate && !Zotero.Proxies.hosts[host] && proxy.hosts.indexOf(host) == -1 && !_isBlacklisted(host)) {
// if no saved host and host is not blacklisted, auto associate
proxy.hosts.push(host);
proxy.save();
proxy.save(true);
}
}
} else if(autoRecognize) {
} else {
// otherwise, try to detect a proxy
var proxy = false;
for each(var detector in Zotero.Proxies.Detectors) {
@ -95,47 +98,27 @@ Zotero.Proxies = new function() {
Components.utils.reportError(e);
}
if (!proxy) {
continue;
}
// if transparent is turned off, just save the proxy
if(!transparent) {
proxy.save();
break;
}
// otherwise, make sure we want it
var io = {site:proxy.hosts[0], proxy:channel.URI.hostPort};
var window = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
window.openDialog('chrome://zotero/content/proxy.xul', '', 'chrome,modal', io);
if(io.add) proxy.save();
if(io.disable) {
transparent = false;
Zotero.Prefs.set("proxies.transparent", false);
}
if(!proxy) continue;
var transparent = _showDialog(proxy.hosts[0], channel.URI.hostPort);
proxy.save(transparent);
break;
}
}
// try to get an applicable proxy
if(transparent) {
var webNav = null;
try {
webNav = channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIWebNavigation);
} catch(e) {}
if(webNav) {
var proxied = this.properToProxy(url, true);
var proxied = Zotero.Proxies.properToProxy(url, true);
if(proxied) {
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
// If the referrer is a proxiable host, we already have access
// (e.g., we're on-campus) and shouldn't redirect
if (channel.referrer) {
if (this.properToProxy(channel.referrer.spec, true)) {
if (Zotero.Proxies.properToProxy(channel.referrer.spec, true)) {
return;
}
}
@ -143,30 +126,22 @@ Zotero.Proxies = new function() {
}
}
}
}
/**
* Gets all proxy objects
* @type Zotero.Proxy[]
*/
this.get = function() {
if(!proxies) {
var rows = Zotero.DB.query("SELECT * FROM proxies");
proxies = [new Zotero.Proxy(row) for each(row in rows)];
this.refreshHostMap();
}
return proxies;
}
/**
* Removes a proxy object from the list of proxy objects
* @returns {Boolean} True if the proxy was in the list, false if it was not
*/
this.remove = function(proxy) {
var index = proxies.indexOf(proxy);
var index = Zotero.Proxies.proxies.indexOf(proxy);
if(index == -1) return false;
proxies.splice(index, 1);
this.refreshHostMap();
// remove proxy from proxy list
Zotero.Proxies.proxies.splice(index, 1);
// remove hosts from host list
for(var host in Zotero.Proxies.hosts) {
if(Zotero.Proxies.hosts[host] == proxy) {
delete Zotero.Proxies.hosts[host];
}
}
return true;
}
@ -174,10 +149,14 @@ Zotero.Proxies = new function() {
* Inserts a proxy into the host map; necessary when proxies are added
*/
this.save = function(proxy) {
proxies.push(proxy);
// add to list of proxies
if(Zotero.Proxies.proxies.indexOf(proxy) == -1) Zotero.Proxies.proxies.push(proxy);
// if there is a proxy ID (i.e., if this is a persisting, transparent proxy), add to host
// list to do reverse mapping
if(proxy.proxyID) {
for each(var host in proxy.hosts) {
if(!hosts[host]) {
hosts[host] = proxy;
Zotero.Proxies.hosts[host] = proxy;
}
}
}
@ -185,19 +164,22 @@ Zotero.Proxies = new function() {
/**
* Refreshes host map; necessary when proxies are changed or deleted
*/
this.refreshHostMap = function() {
hosts = {};
for each(var proxy in proxies) {
for each(var host in proxy.hosts) {
if(!hosts[host]) {
hosts[host] = proxy;
}
this.refreshHostMap = function(proxy) {
// if there is no proxyID, then return immediately, since there is no need to update
if(!proxy.proxyID) return;
// delete hosts that point to this proxy if they no longer exist
for(var host in Zotero.Proxies.hosts) {
if(Zotero.Proxies.hosts[host] == proxy && proxy.hosts.indexOf(host) == -1) {
delete Zotero.Proxies.hosts[host];
}
}
// add new hosts for this proxy
Zotero.Proxies.save(proxy);
}
/**
* Returns a page's proper URL from a proxied URL
* Returns a page's proper URL from a proxied URL. Uses both transparent and opaque proxies.
* @param {String} url
* @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If
* it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the
@ -205,7 +187,7 @@ Zotero.Proxies = new function() {
* @type String
*/
this.proxyToProper = function(url, onlyReturnIfProxied) {
for each(var proxy in proxies) {
for each(var proxy in Zotero.Proxies.proxies) {
if(proxy.regexp) {
var m = proxy.regexp.exec(url);
if(m) {
@ -219,7 +201,7 @@ Zotero.Proxies = new function() {
}
/**
* Returns a page's proxied URL from the proper URL
* Returns a page's proxied URL from the proper URL. Uses only transparent proxies.
* @param {String} url
* @param {Boolean} onlyReturnIfProxied Controls behavior if the given URL is not proxied. If
* it is false or unspecified, unproxied URLs are returned verbatim. If it is true, the
@ -228,13 +210,78 @@ Zotero.Proxies = new function() {
*/
this.properToProxy = function(url, onlyReturnIfProxied) {
var uri = ioService.newURI(url, null, null);
if(hosts[uri.hostPort]) {
var toProxy = hosts[uri.hostPort].toProxy(uri);
if(Zotero.Proxies.hosts[uri.hostPort] && Zotero.Proxies.hosts[uri.hostPort].proxyID) {
var toProxy = Zotero.Proxies.hosts[uri.hostPort].toProxy(uri);
Zotero.debug("Zotero.Proxies.properToProxy: "+url+" to "+toProxy);
return toProxy;
}
return (onlyReturnIfProxied ? false : url);
}
/**
* Determines whether a host is blacklisted, i.e., whether we should refuse to save transparent
* proxy entries for this host. This is necessary because EZProxy offers to proxy all Google and
* Wikipedia subdomains, but in practice, this would get really annoying.
*
* @type Boolean
* @private
*/
function _isBlacklisted(host) {
/**
* Regular expression patterns of hosts never to proxy
* @const
*/
const hostBlacklist = [
/google\.com$/,
/wikipedia\.org$/
];
/**
* Regular expression patterns of hosts that should always be proxied, regardless of whether
* they're on the blacklist
* @const
*/
const hostWhitelist = [
/^scholar\.google\.com$/
]
for each(var blackPattern in hostBlacklist) {
if(blackPattern.test(host)) {
for each(var whitePattern in hostWhitelist) {
if(whitePattern.test(host)) {
return false;
}
}
return true;
}
}
return false;
}
/**
* If transparent is enabled, shows a dialog asking user whether to add a proxy to the
* transparent proxy list.
*
* @param {String} proxiedHost The host that would be redirected through the proxy.
* @param {String} proxyHost The host through which the given site would be redirected.
* @returns {Boolean} True if proxy should be added; false if it should not be.
*/
function _showDialog(proxiedHost, proxyHost) {
if(!Zotero.Proxies.transparent) return false;
// ask user whether to add this proxy
var io = {site:proxiedHost, proxy:proxyHost};
var window = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
window.openDialog('chrome://zotero/content/proxy.xul', '', 'chrome,modal', io);
// disable transparent if checkbox checked
if(io.disable) {
Zotero.Proxies.transparent = false;
Zotero.Prefs.set("proxies.transparent", false);
}
return io.add;
}
}
/**
@ -260,7 +307,7 @@ const Zotero_Proxy_schemeParameters = {
"%p":"(.*?)", // path
"%d":"(.*?)", // directory
"%f":"(.*?)", // filename
"%a":"(.*?)" // filename
"%a":"(.*?)" // anything
};
/**
@ -275,13 +322,13 @@ const Zotero_Proxy_schemeParameterRegexps = {
"%a":/([^%])%a/
};
const Zotero_Proxy_metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g;
/**
* Compiles the regular expression against which we match URLs to determine if this proxy is in use
* and saves it in this.regexp
*/
Zotero.Proxy.prototype.compileRegexp = function() {
const metaRe = /[-[\]{}()*+?.\\^$|,#\s]/g;
// take host only if flagged as multiHost
var parametersToCheck = Zotero_Proxy_schemeParameters;
if(this.multiHost) parametersToCheck["%h"] = "([a-zA-Z0-9]+\\.[a-zA-Z0-9\.]+)";
@ -309,7 +356,7 @@ Zotero.Proxy.prototype.compileRegexp = function() {
})
// now replace with regexp fragment in reverse order
var re = "^"+this.scheme.replace(metaRe, "\\$&")+"$";
var re = "^"+this.scheme.replace(Zotero_Proxy_metaRegexp, "\\$&")+"$";
for(var i=this.parameters.length-1; i>=0; i--) {
var param = this.parameters[i];
re = re.replace(Zotero_Proxy_schemeParameterRegexps[param], "$1"+parametersToCheck[param]);
@ -326,19 +373,30 @@ Zotero.Proxy.prototype.compileRegexp = function() {
*/
Zotero.Proxy.prototype.validate = function() {
if(this.scheme.length < 8 || (this.scheme.substr(0, 7) != "http://" && this.scheme.substr(0, 8) != "https://")) {
return "scheme.noHTTP";
return ["scheme.noHTTP"];
}
if(!this.multiSite && (!this.hosts.length || !this.hosts[0])) {
return "host.invalid";
return ["host.invalid"];
} else if(this.multiSite && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) {
return "scheme.noHost";
return ["scheme.noHost"];
}
if(!Zotero_Proxy_schemeParameterRegexps["%p"].test(this.scheme) &&
(!Zotero_Proxy_schemeParameterRegexps["%d"].test(this.scheme) ||
!Zotero_Proxy_schemeParameterRegexps["%f"].test(this.scheme))) {
return "scheme.noPath";
return ["scheme.noPath"];
}
if(this.scheme.substr(0, 10) == "http://%h/" || this.scheme.substr(0, 11) == "https://%h/") {
return ["scheme.invalid"];
}
for each(var host in this.hosts) {
var oldHost = Zotero.Proxies.hosts[host];
if(oldHost && oldHost.proxyID && oldHost != this) {
return ["host.proxyExists", host];
}
}
return false;
@ -346,21 +404,25 @@ Zotero.Proxy.prototype.validate = function() {
/**
* Saves any changes to this proxy
*
* @param {Boolean} transparent True if proxy should be saved as a persisting, transparent proxy
*/
Zotero.Proxy.prototype.save = function() {
Zotero.Proxy.prototype.save = function(transparent) {
// ensure this proxy is valid
Zotero.debug(this);
var hasErrors = this.validate();
if(hasErrors) throw "Proxy could not be saved because it is invalid: error "+hasErrors;
if(hasErrors) throw "Zotero.Proxy: could not be saved because it is invalid: error "+hasErrors[0];
this.autoAssociate = this.multiHost && this.autoAssociate;
this.compileRegexp();
if(this.proxyID) {
Zotero.Proxies.refreshHostMap();
Zotero.Proxies.refreshHostMap(this);
if(!transparent) throw "Zotero.Proxy: cannot save transparent proxy without transparent param";
} else {
Zotero.Proxies.save(this);
}
if(transparent) {
try {
Zotero.DB.beginTransaction();
@ -387,6 +449,7 @@ Zotero.Proxy.prototype.save = function() {
throw(e);
}
}
}
/**
* Reverts to the previously saved version of this proxy
@ -400,9 +463,9 @@ Zotero.Proxy.prototype.revert = function() {
* Deletes this proxy
*/
Zotero.Proxy.prototype.erase = function() {
if(!this.proxyID) throw "Tried to erase an unsaved proxy";
Zotero.Proxies.remove(this);
if(this.proxyID) {
try {
Zotero.DB.beginTransaction();
Zotero.DB.query("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]);
@ -413,6 +476,7 @@ Zotero.Proxy.prototype.erase = function() {
throw(e);
}
}
}
/**
* Converts a proxied URL to an unproxied URL using this proxy
@ -514,7 +578,7 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
if(fromProxy && toProxy && fromProxy.host == toProxy.host && fromProxy.port != toProxy.port
&& [80, 443, -1].indexOf(toProxy.port) == -1) {
var proxy;
for each(proxy in Zotero.Proxies.get()) {
for each(proxy in Zotero.Proxies.proxies) {
if(proxy.regexp) {
var m = proxy.regexp.exec(fromProxy.spec);
if(m) break;
@ -569,7 +633,7 @@ Zotero.Proxies.Detectors.EZProxy = function(channel) {
proxy.multiHost = false;
proxy.scheme = proxiedURI.scheme+"://"+proxiedURI.hostPort+"/%p";
proxy.hosts = [properURI.hostPort];
} else if(proxiedURI.host != channel.URI.host) {
} else if(proxiedURI.host != channel.URI.host && proxiedURI.hostPort.indexOf(properURI.host) != -1) {
// Proxy by host
var proxy = new Zotero.Proxy();
proxy.multiHost = proxy.autoAssociate = true;
@ -628,7 +692,8 @@ Zotero.Proxies.Detectors.Juniper = function(channel) {
if(!m) return false;
var proxy = new Zotero.Proxy();
proxy.multiHost = proxy.autoAssociate = true;
proxy.multiHost = true;
proxy.autoAssociate = false;
proxy.scheme = m[1]+"/%d"+",DanaInfo=%h%a+%f";
proxy.hosts = [m[3]];
return proxy;

View file

@ -2194,6 +2194,11 @@ Zotero.Schema = new function(){
Zotero.DB.query("DROP TABLE savedSearchesOld");
Zotero.DB.query("DROP TABLE tagsOld");
}
if (i==50) {
Zotero.DB.query("DELETE FROM proxyHosts");
Zotero.DB.query("DELETE FROM proxies");
}
}
_updateDBVersion('userdata', toVersion);

View file

@ -87,8 +87,10 @@
<!ENTITY zotero.preferences.prefpane.proxies "Proxies">
<!ENTITY zotero.preferences.proxies.proxyOptions "Proxy Options">
<!ENTITY zotero.preferences.proxies.autoRecognize "Automatically recognize common URL-rewriting proxy systems">
<!ENTITY zotero.preferences.proxies.transparent "Transparently redirect requests through previously used proxies">
<!ENTITY zotero.preferences.proxies.desc_before_link "Zotero will transparently redirect requests through saved proxies. See the">
<!ENTITY zotero.preferences.proxies.desc_link "proxy documentation">
<!ENTITY zotero.preferences.proxies.desc_after_link "for more information.">
<!ENTITY zotero.preferences.proxies.transparent "Automatically remember proxied resources">
<!ENTITY zotero.preferences.proxies.configured "Configured Proxies">
<!ENTITY zotero.preferences.proxies.hostname "Hostname">
<!ENTITY zotero.preferences.proxies.scheme "Scheme">

View file

@ -519,10 +519,10 @@ proxies.error.scheme.noHTTP = Valid proxy schemes must start with "http://" or
proxies.error.host.invalid = You must enter a full hostname for the site served by this proxy (e.g., jstor.org).
proxies.error.scheme.noHost = A multi-site proxy scheme must contain the host variable (%h).
proxies.error.scheme.noPath = A valid proxy scheme must contain either the path variable (%p) or the directory and filename variables (%d and %f).
proxies.error.host.proxyExists = You have already defined another proxy for the host %1$S.
proxies.error.scheme.invalid = The entered proxy scheme is invalid; it would apply to all hosts.
proxies.recognized.message = Adding this proxy will allow Zotero to recognize items from its pages and will automatically redirect future requests to %1$S through %2$S.
proxies.recognized.add = Add Proxy
proxies.enableTransparentWarning.title = Warning
proxies.enableTransparentWarning.description = Please ensure that the proxies listed below belong to a library, school, or other institution with which you are affiliated. A malicious proxy could pose a security risk.
recognizePDF.noOCR = PDF does not contain OCRed text.
recognizePDF.couldNotRead = Could not read text from PDF.

View file

@ -104,7 +104,6 @@ pref("extensions.zotero.sync.storage.maxUploads", 4);
pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
// Proxy
pref("extensions.zotero.proxies.autoRecognize", true);
pref("extensions.zotero.proxies.transparent", true);
// Data layer purging

View file

@ -1,4 +1,4 @@
-- 49
-- 50
-- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema()