commit 09e8249db80ef3619cac02e41856aa77f0dc12aa parent 30e4ae859da12904167803ecff4dfcfc9198014f Author: Simon Kornblith <simon@simonster.com> Date: Mon, 28 Jul 2008 11:11:12 +0000 closes #743, Support non-EZproxy proxies closes #831, transparent EZProxy support adds a proxy pane to the preferences asks before saving proxies to the DB (to avoid the potential phishing risk #831 would otherwise pose) Diffstat:
16 files changed, 898 insertions(+), 197 deletions(-)
diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js @@ -103,7 +103,7 @@ var Zotero_Browser = new function() { Zotero_Browser.browserData = new Object(); Zotero_Browser._scrapePopupShowing = false; - Zotero.Ingester.ProxyMonitor.init(); + Zotero.Proxies.init(); Zotero.Ingester.MIMEHandler.init(); Zotero.Cite.MIMEHandler.init(); Zotero.Translate.init(); diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js @@ -22,6 +22,7 @@ var openURLServerField; var openURLVersionMenu; +var proxies; function init() { @@ -32,6 +33,7 @@ function init() } refreshStylesList(); + refreshProxyList(); populateQuickCopyList(); updateQuickCopyInstructions(); initSearchPane(); @@ -930,8 +932,6 @@ function refreshStylesList(cslID) { var styleData = Zotero.DB.query(sql); if (!styleData) return; - Zotero.debug("ASKED FOR "+cslID); - var selectIndex = false; for (var i=0; i<styleData.length; i++) { var treeitem = document.createElement('treeitem'); @@ -949,7 +949,6 @@ function refreshStylesList(cslID) { if(styleData[i].cslID.length < Zotero.ENConverter.uriPrefix.length || styleData[i].cslID.substr(0, Zotero.ENConverter.uriPrefix.length) != Zotero.ENConverter.uriPrefix) { cslCell.setAttribute('src', 'chrome://zotero/skin/tick.png'); - Zotero.debug("ISCSL"); } treerow.appendChild(titleCell); @@ -1029,12 +1028,13 @@ function addStyle() { **/ function deleteStyle() { var tree = document.getElementById('styleManager'); + if(tree.currentIndex == -1) return; var treeitem = tree.lastChild.childNodes[tree.currentIndex]; - Zotero.debug(treeitem.getAttribute('id')); var cslID = treeitem.getAttribute('id').substr(11); Zotero.Cite.deleteStyle(cslID); this.refreshStylesList(); + document.getElementById('styleManager-delete').disabled = true; } /** @@ -1042,4 +1042,80 @@ function deleteStyle() { **/ function styleImportError() { alert(Zotero.getString('styles.installError', "This")); +} + +/** + * Adds a proxy to the proxy pane + */ +function showProxyEditor(index) { + if(index == -1) return; + window.openDialog('chrome://zotero/content/preferences/proxyEditor.xul', + "zotero-preferences-proxyEditor", "chrome, modal", index !== undefined ? proxies[index] : null); + refreshProxyList(); +} + +/** + * Deletes the currently selected proxy + */ +function deleteProxy() { + if(document.getElementById('proxyTree').currentIndex == -1) return; + proxies[document.getElementById('proxyTree').currentIndex].erase(); + refreshProxyList(); + document.getElementById('proxyTree-delete').disabled = true; +} + +/** + * Refreshes the proxy pane + */ +function refreshProxyList() { + // get and sort proxies + proxies = Zotero.Proxies.get(); + proxies = proxies.sort(function(a, b) { + if(a.multiHost) { + if(b.multiHost) { + if(a.hosts[0] < b.hosts[0]) { + return -1; + } else { + return 1; + } + } else { + return -1; + } + } else if(b.multiHost) { + return 1; + } + + if(a.scheme < b.scheme) { + return -1; + } else if(b.scheme > a.scheme) { + return 1; + } + + return 0; + }); + + // erase old children + var treechildren = document.getElementById('proxyTree-rows'); + while (treechildren.hasChildNodes()) { + treechildren.removeChild(treechildren.firstChild); + } + + // add proxies to list + for (var i=0; i<proxies.length; i++) { + var treeitem = document.createElement('treeitem'); + var treerow = document.createElement('treerow'); + var hostnameCell = document.createElement('treecell'); + var schemeCell = document.createElement('treecell'); + + hostnameCell.setAttribute('label', proxies[i].multiHost ? Zotero.getString("proxies.multiSite") : proxies[i].hosts[0]); + schemeCell.setAttribute('label', proxies[i].scheme); + + treerow.appendChild(hostnameCell); + treerow.appendChild(schemeCell); + treeitem.appendChild(treerow); + treechildren.appendChild(treeitem); + } + + document.getElementById('proxyTree').currentIndex = -1; + document.getElementById('proxyTree-delete').disabled = true; } \ No newline at end of file diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul @@ -334,6 +334,7 @@ To add a new preference: <caption label="&zotero.preferences.styles.styleManager;"/> <tree flex="1" id="styleManager" hidecolumnpicker="true" rows="6" + onselect="document.getElementById('styleManager-delete').disabled = undefined" onkeypress="if (event.keyCode == event.DOM_VK_DELETE) { deleteSelectedStyle(); }"> <treecols> <treecol id="styleManager-title" label="&zotero.preferences.styles.styleManager.title;" flex="3"/> @@ -344,7 +345,7 @@ To add a new preference: </tree> <separator class="thin"/> <hbox pack="end"> - <button label="-" onclick="deleteStyle()"/> + <button disabled="true" id="styleManager-delete" label="-" onclick="deleteStyle()"/> <button label="+" onclick="addStyle()"/> </hbox> <separator/> @@ -353,6 +354,44 @@ To add a new preference: </prefpane> + <prefpane id="zotero-prefpane-proxies" + label="&zotero.preferences.prefpane.proxies;" + image="chrome://zotero/skin/prefs-proxies.png"> + <preferences> + <preference id="pref-proxies-autoRecognize" name="extensions.zotero.proxies.autoRecognize" type="bool"/> + <preference id="pref-proxies-transparent" name="extensions.zotero.proxies.transparent" type="bool"/> + </preferences> + + <groupbox> + <caption label="&zotero.preferences.proxies.proxyOptions;"/> + + <checkbox id="zotero-proxies-autoRecognize" label="&zotero.preferences.proxies.autoRecognize;" + preference="pref-proxies-autoRecognize" oncommand="Zotero.Proxies.init()"/> + <checkbox id="zotero-proxies-transparent" label="&zotero.preferences.proxies.transparent;" + preference="pref-proxies-transparent" oncommand="Zotero.Proxies.init()"/> + </groupbox> + + <groupbox flex="1"> + <caption label="&zotero.preferences.proxies.configured;"/> + + <tree flex="1" id="proxyTree" hidecolumnpicker="true" rows="6" seltype="single" + ondblclick="showProxyEditor(this.currentIndex)" onselect="document.getElementById('proxyTree-delete').disabled = undefined" + onkeypress="if (event.keyCode == event.DOM_VK_DELETE) { deleteProxy(); }"> + <treecols> + <treecol id="proxyTree-hostname" label="&zotero.preferences.proxies.hostname;" flex="1"/> + <treecol id="proxyTree-scheme" label="&zotero.preferences.proxies.scheme;" flex="3"/> + </treecols> + <treechildren id="proxyTree-rows"/> + </tree> + <separator class="thin"/> + <hbox pack="end"> + <button disabled="true" id="proxyTree-delete" label="-" onclick="deleteProxy()"/> + <button label="+" onclick="showProxyEditor()"/> + </hbox> + </groupbox> + </prefpane> + + <prefpane id="zotero-prefpane-keys" label="&zotero.preferences.prefpane.keys;" image="chrome://zotero/skin/prefs-keys.png"> diff --git a/chrome/content/zotero/preferences/proxyEditor.js b/chrome/content/zotero/preferences/proxyEditor.js @@ -0,0 +1,137 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero_ProxyEditor = new function() { + var treechildren; + var tree; + var treecol; + var multiSite; + + /** + * Called when this window is first opened. Sets values if necessary + */ + this.load = function() { + treechildren = document.getElementById("zotero-proxies-hostname-multiSite-tree-children"); + tree = document.getElementById("zotero-proxies-hostname-multiSite-tree"); + multiSite = document.getElementById("zotero-proxies-multiSite"); + + if(window.arguments && window.arguments[0]) { + var proxy = window.arguments[0]; + document.getElementById("zotero-proxies-scheme").value = proxy.scheme; + document.getElementById("zotero-proxies-multiSite").checked = !!proxy.multiHost; + if(proxy.hosts) { + if(proxy.multiHost) { + this.multiSiteChanged(); + for (var i=0; i<proxy.hosts.length; i++) { + _addTreeElement(proxy.hosts[i]); + } + document.getElementById("zotero-proxies-autoAssociate").checked = proxy.autoAssociate; + } else { + document.getElementById("zotero-proxies-hostname-text").value = proxy.hosts[0]; + } + } + } + + window.sizeToContent(); + } + + /** + * Called when a user checks/unchecks the Multi-Site checkbox. Shows or hides multi-site + * hostname specification box as necessary. + */ + this.multiSiteChanged = function() { + document.getElementById("zotero-proxies-hostname-multiSite").hidden = !multiSite.checked; + document.getElementById("zotero-proxies-hostname-multiSite-description").hidden = !multiSite.checked; + document.getElementById("zotero-proxies-hostname").hidden = multiSite.checked; + window.sizeToContent(); + } + + /** + * Called when a row is selected + */ + this.select = function() { + document.getElementById("zotero-proxies-delete").disabled = tree.selectedIndex == -1; + } + + /** + * Adds a host when in multi-host mode + */ + this.addHost = function() { + _addTreeElement(); + tree.startEditing(treechildren.childNodes.length-1, tree.columns.getFirstColumn()); + } + + /** + * Deletes a host when in multi-host mode + */ + this.deleteHost = function() { + if(tree.currentIndex == -1) return; + treechildren.removeChild(treechildren.childNodes[tree.currentIndex]); + document.getElementById("zotero-proxies-delete").disabled = true; + } + + /** + * Called when the user clicks "OK." Updates proxy for Zotero.Proxy. + */ + this.accept = function() { + var proxy = window.arguments && window.arguments[0] ? window.arguments[0] : new Zotero.Proxy(); + + proxy.scheme = document.getElementById("zotero-proxies-scheme").value; + proxy.multiHost = multiSite.checked; + if(proxy.multiHost) { + proxy.hosts = []; + var treecol = tree.columns.getFirstColumn(); + for(var i=0; i<tree.view.rowCount; i++) { + var host = tree.view.getCellText(i, treecol); + if(host) proxy.hosts.push(host); + } + proxy.autoAssociate = document.getElementById("zotero-proxies-autoAssociate").checked; + } else { + proxy.hosts = [document.getElementById("zotero-proxies-hostname-text").value]; + } + + var hasErrors = proxy.validate(); + if(hasErrors) { + Components.interfaces.nsIPromptService.getService().alert(window, + Zotero.getString("proxies.error"), Zotero.getString("proxies.error."+hasErrors)); + if(window.arguments && window.arguments[0]) proxy.revert(); + return false; + } + proxy.save(); + return true; + } + + /** + * Adds an element to the tree + */ + function _addTreeElement(label) { + var treeitem = document.createElement('treeitem'); + var treerow = document.createElement('treerow'); + var treecell = document.createElement('treecell'); + + if(label) treecell.setAttribute('label', label); + + treerow.appendChild(treecell); + treeitem.appendChild(treerow); + treechildren.appendChild(treeitem); + } +} +\ No newline at end of file diff --git a/chrome/content/zotero/preferences/proxyEditor.xul b/chrome/content/zotero/preferences/proxyEditor.xul @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://zotero/skin/preferences.css"?> + +<!DOCTYPE window SYSTEM "chrome://zotero/locale/preferences.dtd"> +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="" buttons="cancel,accept" + id="zotero-proxyEditor" + onload="Zotero_ProxyEditor.load();" + ondialogaccept="return Zotero_ProxyEditor.accept();"> + + <script src="chrome://zotero/content/include.js"/> + <script src="proxyEditor.js"/> + + <checkbox id="zotero-proxies-multiSite" label="&zotero.preferences.proxies.multiSite;" + oncommand="Zotero_ProxyEditor.multiSiteChanged()"/> + <separator class="thin"/> + <vbox id="zotero-proxies-hostname-multiSite" hidden="true"> + <checkbox id="zotero-proxies-autoAssociate" label="&zotero.preferences.proxies.autoAssociate;"/> + <tree flex="1" id="zotero-proxies-hostname-multiSite-tree" hidecolumnpicker="true" editable="true" rows="6" + onkeypress="if (event.keyCode == event.DOM_VK_DELETE) { Zotero_ProxyEditor.remove(); }" + onselect="Zotero_ProxyEditor.select();"> + <treecols> + <treecol label="&zotero.preferences.proxies.hostname;" id="zotero-proxies-hostname-multiSite-tree-col" flex="1"/> + </treecols> + <treechildren id="zotero-proxies-hostname-multiSite-tree-children"/> + </tree> + <hbox pack="end"> + <button id="zotero-proxies-delete" label="-" onclick="Zotero_ProxyEditor.deleteHost()" disabled="true"/> + <button id="zotero-proxies-add" label="+" onclick="Zotero_ProxyEditor.addHost()"/> + </hbox> + </vbox> + <vbox id="zotero-proxies-hostname"> + <label value="&zotero.preferences.proxies.hostname;:" control="zotero-proxies-hostname-text"/> + <textbox id="zotero-proxies-hostname-text"/> + </vbox> + <separator class="thin"/> + <label value="&zotero.preferences.proxies.scheme;:" control="zotero-proxies-scheme"/> + <textbox id="zotero-proxies-scheme"/> + <label value="&zotero.preferences.proxies.variables;"/> + <label value="&zotero.preferences.proxies.h_variable;" id="zotero-proxies-hostname-multiSite-description" hidden="true"/> + <label value="&zotero.preferences.proxies.p_variable;"/> + <label value="&zotero.preferences.proxies.d_variable;"/> + <label value="&zotero.preferences.proxies.f_variable;"/> + <label value="&zotero.preferences.proxies.a_variable;"/> +</dialog> diff --git a/chrome/content/zotero/xpcom/ingester.js b/chrome/content/zotero/xpcom/ingester.js @@ -26,185 +26,6 @@ Zotero.Ingester = new Object(); -///////////////////////////////////////////////////////////////// -// -// Zotero.Ingester.ProxyMonitor -// -///////////////////////////////////////////////////////////////// - -// A singleton for recognizing EZProxies and converting URLs such that databases -// will work from outside them. Unfortunately, this only works with the ($495) -// EZProxy software. If there are open source alternatives, we should support -// them too. - -/* - * Precompile proxy regexps - */ -Zotero.Ingester.ProxyMonitor = new function() { - var _ezProxyRe = /\?(?:.+&)?(url|qurl)=([^&]+)/i; - var _juniperProxyRe = /^(https?:\/\/[^\/:]+(?:\:[0-9]+)?)\/(.*)?,DanaInfo=([^+,]*)([^+]*)(?:\+(.*))?$/; - var _pathRe = /([^?]*\/)([^?\/]*)(\?(.*))?$/ - /*var _hostRe = new RegExp(); - _hostRe.compile("^https?://(([^/:]+)(?:\:([0-9]+))?)");*/ - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - var on = false; - var _mapFromEZProxy = null; - var _mapToJuniperProxy = null; - var _mapToEZProxy = null; - - this.init = init; - this.proxyToProper = proxyToProper; - this.properToProxy = properToProxy; - this.observe = observe; - - function init() { - if(!on) { - var observerService = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - observerService.addObserver(this, "http-on-examine-response", false); - } - on = true; - } - - function observe(channel) { - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - try { - // remove content-disposition headers for endnote, etc. - var contentType = channel.getResponseHeader("Content-Type").toLowerCase(); - for each(var desiredContentType in Zotero.Ingester.MIMEHandler.URIContentListener.desiredContentTypes) { - if(contentType.length < desiredContentType.length) { - break; - } else { - if(contentType.substr(0, desiredContentType.length) == desiredContentType) { - channel.setResponseHeader("Content-Disposition", "", false); - break; - } - } - } - } catch(e) {} - - try { - // find ezproxies - if(channel.getResponseHeader("Server") == "EZproxy") { - // We're connected to an EZproxy - if(channel.responseStatus != "302") { - return; - } - - // We should be able to scrape the URL out of this - var m = _ezProxyRe.exec(channel.URI.spec); - if(!m) { - return; - } - - // Found URL - var variable = m[1]; - var properURL = m[2]; - if(variable.toLowerCase() == "qurl") { - properURL = unescape(properURL); - } - var properURI = _parseURL(properURL); - if(!properURI) { - return; - } - - // Get the new URL - var newURL = channel.getResponseHeader("Location"); - if(!newURL) { - return; - } - var newURI = _parseURL(newURL); - if(!newURI) { - return; - } - - if((channel.URI.host == newURI.host && channel.URI.port != newURI.port) || - (newURI.host != channel.URI.host && - newURI.hostPort.substr(newURI.hostPort.length-channel.URI.hostPort.length) == channel.URI.hostPort)) { - // Different ports but the same server means EZproxy active - - Zotero.debug("EZProxy: host "+newURI.hostPort+" is really "+properURI.hostPort); - // Initialize variables here so people who never use EZProxies - // don't get the (very very minor) speed hit - if(!_mapFromEZProxy) { - _mapFromEZProxy = new Object(); - _mapToEZProxy = new Object(); - } - _mapFromEZProxy[newURI.hostPort] = properURI.hostPort; - _mapToEZProxy[properURI.hostPort] = newURI.hostPort; - } - } - } catch(e) {} - } - - /* - * Returns a page's proper url, adjusting for proxying - */ - function proxyToProper(url) { - var m = _juniperProxyRe.exec(url); - if(m) { - url = "http://"+m[3]+"/"+m[2]+m[5]; - - if(!_mapToJuniperProxy) _mapToJuniperProxy = new Object(); - _mapToJuniperProxy[m[3]] = {prePath:m[1], additionalInfo:m[4], danaInfoBeforeFile:(m[2].substr(m[2].length-1) == "/")}; - - Zotero.debug("Juniper Proxy: proper url is "+url); - } else if(_mapFromEZProxy) { - // EZProxy detection is active - - var uri = _parseURL(url); - if(uri && _mapFromEZProxy[uri.hostPort]) { - url = url.replace(uri.hostPort, _mapFromEZProxy[uri.hostPort]); - Zotero.debug("EZProxy: proper url is "+url); - } - } - - return url; - } - - /* - * Returns a page's proxied url from the proper url - */ - function properToProxy(url) { - if(_mapToJuniperProxy || _mapToEZProxy) { - // Proxy detection is active - var uri = _parseURL(url); - - if(uri) { - if(_mapToEZProxy && _mapToEZProxy[uri.hostPort]) { - // Actually need to map - url = url.replace(uri.hostPort, _mapToEZProxy[uri.hostPort]); - Zotero.debug("EZProxy: proxied url is "+url); - } else if(_mapToJuniperProxy && _mapToJuniperProxy[uri.hostPort]) { - var m = _pathRe.exec(uri.path); - - if(_mapToJuniperProxy[uri.hostPort].danaInfoBeforeFile) { - url = _mapToJuniperProxy[uri.hostPort].prePath+m[1]+",DanaInfo="+uri.hostPort+_mapToJuniperProxy[uri.hostPort].additionalInfo+"+"; - if(m[2]) url += m[2]; - } else { - url = _mapToJuniperProxy[uri.hostPort].prePath+m[1]+m[2]+",DanaInfo="+uri.hostPort+_mapToJuniperProxy[uri.hostPort].additionalInfo+"+"; - } - if(m[3]) url += m[3]; - Zotero.debug("Juniper Proxy: proxied url is "+url); - } - } - } - - return url; - } - - /* - * Parses a url into components (hostPort, port, host, and spec) - */ - function _parseURL(url) { - // create an nsIURI (not sure if this is faster than the regular - // expression, but it's at least more kosher) - var uri = ioService.newURI(url, null, null); - return uri; - } -} - Zotero.OpenURL = new function() { this.resolve = resolve; this.discoverResolvers = discoverResolvers; diff --git a/chrome/content/zotero/xpcom/proxy.js b/chrome/content/zotero/xpcom/proxy.js @@ -0,0 +1,526 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + 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 + */ +Zotero.Proxies = new function() { + var on = false; + var proxies = false; + var hosts; + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + var autoRecognize = false; + var transparent = false; + + this.init = function() { + if(!on) { + var observerService = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + observerService.addObserver(this, "http-on-examine-response", false); + this.get(); + } + on = true; + + autoRecognize = Zotero.Prefs.get("proxies.autoRecognize"); + transparent = Zotero.Prefs.get("proxies.transparent"); + } + + /** + * Observe method to capture page loads + */ + this.observe = function(channel) { + channel.QueryInterface(Components.interfaces.nsIHttpChannel); + try { + // remove content-disposition headers for endnote, etc. + var contentType = channel.getResponseHeader("Content-Type").toLowerCase(); + for each(var desiredContentType in Zotero.Ingester.MIMEHandler.URIContentListener.desiredContentTypes) { + if(contentType.length < desiredContentType.length) { + break; + } else { + if(contentType.substr(0, desiredContentType.length) == desiredContentType) { + channel.setResponseHeader("Content-Disposition", "", false); + break; + } + } + } + } catch(e) {} + + // try to detect a proxy + channel.QueryInterface(Components.interfaces.nsIRequest); + if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) { + channel.QueryInterface(Components.interfaces.nsIHttpChannel); + var url = channel.URI.spec; + + // see if there is a proxy we already know + var proxy; + for each(proxy in proxies) { + if(proxy.regexp && proxy.autoAssociate) { + var m = proxy.regexp.exec(url); + if(m) break; + } + } + + if(m) { + // add this host if we know a proxy + var host = m[proxy.parameters.indexOf("%h")+1]; + if(proxy.hosts.indexOf(host) == -1) { + proxy.hosts.push(host); + proxy.save(); + } + } else if(autoRecognize) { + // otherwise, try to detect a proxy + var proxy = false; + for each(var detector in Zotero.Proxies.Detectors) { + try { + proxy = detector(channel); + } catch(e) { + Components.utils.reportError(e); + } + + if(proxy) { + var checkState = {value:false}; + var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + var window = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + + var button = ps.confirmEx(window, + Zotero.getString("proxies.recognized"), + Zotero.getString("proxies.recognized.message"), + ((proxies.length ? 0 : ps.BUTTON_DELAY_ENABLE) + ps.BUTTON_POS_0 * ps.BUTTON_TITLE_OK + + ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL + ps.BUTTON_POS_1_DEFAULT), + null, null, null, Zotero.getString("proxies.recognized.disable"), checkState); + + if(button == 0) proxy.save(); + if(checkState.value) { + autoRecognize = false; + Zotero.Prefs.set("proxies.autoRecognize", false); + } + + 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); + if(proxied) webNav.loadURI(proxied, 0, channel.URI, null, null); + } + } + } + + delete channel; + } + + /** + * Gets all proxy objects + */ + 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 + */ + this.remove = function(proxy) { + var index = proxies.indexOf(proxy); + if(index == -1) return false; + proxies.splice(index, 1); + this.refreshHostMap(); + return true; + } + + /** + * Saves a proxy object not previously in the proxy list + */ + this.save = function(proxy) { + proxies.push(proxy); + for each(var host in proxy.hosts) { + if(!hosts[host]) { + hosts[host] = proxy; + } + } + return proxy; + } + + /** + * Refreshes host map; necessary when proxies are added, 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; + } + } + } + } + + /** + * Returns a page's proper url, adjusting for proxying + */ + this.proxyToProper = function(url, onlyReturnIfProxied) { + for each(var proxy in proxies) { + if(proxy.regexp) { + var m = proxy.regexp.exec(url); + if(m) { + var toProper = proxy.toProper(m); + Zotero.debug("Zotero.Proxies.proxyToProper: "+url+" to "+toProper); + return toProper; + } + } + } + return (onlyReturnIfProxied ? false : url); + } + + /** + * Returns a page's proxied url from the proper url + */ + this.properToProxy = function(url, onlyReturnIfProxied) { + var uri = ioService.newURI(url, null, null); + if(hosts[uri.hostPort]) { + var toProxy = hosts[uri.hostPort].toProxy(uri); + Zotero.debug("Zotero.Proxies.properToProxy: "+url+" to "+toProxy); + return toProxy; + } + return (onlyReturnIfProxied ? false : url); + } +} + +/** + * A class to handle individual proxy servers + * + * @constructor + */ +Zotero.Proxy = function(row) { + if(row) { + this._loadFromRow(row); + } else { + this.hosts = []; + this.multiHost = false; + } +} + +const Zotero_Proxy_schemeParameters = { + "%p":"(.*?)", // path + "%d":"(.*?)", // directory + "%f":"(.*?)", // filename + "%a":"(.*?)" // filename +}; +const Zotero_Proxy_schemeParameterRegexps = { + "%p":/([^%])%p/, + "%d":/([^%])%d/, + "%f":/([^%])%f/, + "%h":/([^%])%h/, + "%a":/([^%])%a/ +} +/** + * Compiles the regular expression against which we match URLs for this proxy + */ +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\.]+)"; + + indices = this.indices = {}; + this.parameters = []; + for(var param in parametersToCheck) { + var index = this.scheme.indexOf(param); + + // avoid escaped matches + while(this.scheme[index-1] == "%") { + this.scheme = this.scheme.substr(0, index-1)+this.scheme.substr(index); + index = this.scheme.indexOf(param, index+1); + } + + if(index != -1) { + this.indices[param] = index; + this.parameters.push(param); + } + } + + // sort params by index + this.parameters = this.parameters.sort(function(a, b) { + return indices[a]-indices[b]; + }) + + // now replace with regexp fragment in reverse order + var re = "^"+this.scheme.replace(metaRe, "\\$&")+"$"; + 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]); + } + + this.regexp = new RegExp(re); +} + +/** + * Ensures that the proxy scheme and host settings are valid for this proxy type + */ +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"; + } + + if(!this.multiSite && (!this.hosts.length || !this.hosts[0])) { + return "host.invalid"; + } else if(this.multiSite && !Zotero_Proxy_schemeParameterRegexps["%h"].test(this.scheme)) { + 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 false; +} + +/** + * Saves any changes to this proxy + */ +Zotero.Proxy.prototype.save = function() { + // 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; + + this.autoAssociate = this.multiHost && this.autoAssociate; + this.compileRegexp(); + if(this.proxyID) { + Zotero.Proxies.refreshHostMap(); + } else { + Zotero.Proxies.save(this); + } + + try { + Zotero.DB.beginTransaction(); + + if(this.proxyID) { + Zotero.DB.query("UPDATE proxies SET multiHost = ?, autoAssociate = ?, scheme = ? WHERE proxyID = ?", + [this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme, this.proxyID]); + Zotero.DB.query("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]); + } else { + this.proxyID = Zotero.DB.query("INSERT INTO proxies (multiHost, autoAssociate, scheme) VALUES (?, ?, ?)", + [this.multiHost ? 1 : 0, this.autoAssociate ? 1 : 0, this.scheme]) + } + + this.hosts = this.hosts.sort(); + var host; + for(var i in this.hosts) { + host = this.hosts[i] = this.hosts[i].toLowerCase(); + Zotero.DB.query("INSERT INTO proxyHosts (proxyID, hostname) VALUES (?, ?)", + [this.proxyID, host]); + } + + Zotero.DB.commitTransaction(); + } catch(e) { + Zotero.DB.rollbackTransaction(); + throw(e); + } +} + +/** + * Reverts to the previously saved version of this proxy + */ +Zotero.Proxy.prototype.revert = function() { + if(!this.proxyID) throw "Cannot revert an unsaved proxy"; + this._loadFromRow(Zotero.DB.rowQuery("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID])); +} + +/** + * Deletes this proxy + */ +Zotero.Proxy.prototype.erase = function() { + if(!this.proxyID) throw "Tried to erase an unsaved proxy"; + Zotero.Proxies.remove(this); + + try { + Zotero.DB.beginTransaction(); + Zotero.DB.query("DELETE FROM proxies WHERE proxyID = ?", [this.proxyID]); + Zotero.DB.query("DELETE FROM proxyHosts WHERE proxyID = ?", [this.proxyID]); + Zotero.DB.commitTransaction(); + } catch(e) { + Zotero.DB.rollbackTransaction(); + throw(e); + } +} + +/** + * Converts a proxied URL to an unproxied URL using this proxy + * + * @param m {Array} The match from running this proxy's regexp against a URL spec + */ +Zotero.Proxy.prototype.toProper = function(m) { + if(this.multiHost) { + properURL = "http://"+m[this.parameters.indexOf("%h")+1]+"/"; + } else { + properURL = "http://"+this.hosts[0]+"/"; + } + + if(this.indices["%p"]) { + properURL += m[this.parameters.indexOf("%p")+1]; + } else { + var dir = m[this.parameters.indexOf("%d")+1]; + var file = m[this.parameters.indexOf("%f")+1]; + if(dir !== "") properURL += dir+"/"; + properURL += file; + } + + return properURL; +} + +/** + * Converts an unproxied URL to a proxied URL using this proxy + * + * @param {nsIURI} uri The nsIURI corresponding to the unproxied URL + */ +Zotero.Proxy.prototype.toProxy = function(uri) { + proxyURL = this.scheme; + + for(var i=this.parameters.length-1; i>=0; i--) { + var param = this.parameters[i]; + var value = ""; + if(param == "%h") { + value = uri.hostPort; + } else if(param == "%p") { + value = uri.path.substr(1); + } else if(param == "%d") { + value = uri.path.substr(0, uri.path.lastIndexOf("/")); + } else if(param == "%f") { + value = uri.path.substr(uri.path.lastIndexOf("/")+1) + } + + proxyURL = proxyURL.substr(0, this.indices[param])+value+proxyURL.substr(this.indices[param]+2); + } + + return proxyURL; +} + +/** + * Loads a proxy object from a DB row + */ +Zotero.Proxy.prototype._loadFromRow = function(row) { + this.proxyID = row.proxyID; + this.multiHost = !!row.multiHost; + this.autoAssociate = !!row.autoAssociate; + this.scheme = row.scheme; + this.hosts = Zotero.DB.columnQuery("SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", row.proxyID); + this.compileRegexp(); +} + +Zotero.Proxies.Detectors = new Object(); + +/** + * Detector for OCLC EZProxy + */ +Zotero.Proxies.Detectors.EZProxy = function(channel) { + const ezProxyRe = /\?(?:.+&)?(url|qurl)=([^&]+)/i; + try { + if(channel.getResponseHeader("Server") != "EZproxy" || channel.responseStatus != "302") { + return false; + } + } catch(e) { + return false + } + + // We should be able to scrape the URL out of this + var m = ezProxyRe.exec(channel.URI.spec); + if(!m) return false; + + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + + // Found URL + var properURL = m[2]; + if(m[1].toLowerCase() == "qurl") properURL = unescape(properURL); + var properURI = ioService.newURI(properURL, null, null); + if(!properURI) return false; + + // Get the new URL + var newURL = channel.getResponseHeader("Location"); + if(!newURL) return false; + + // Ignore if we already know about it + if(Zotero.Proxies.proxyToProper(newURL, true)) return false; + + // parse into nsIURI + var newURI = ioService.newURI(newURL, null, null); + if(!newURI) return false; + + if(channel.URI.host == newURI.host && channel.URI.port != newURI.port) { + // Old style per-port + var proxy = new Zotero.Proxy(); + proxy.multiHost = false; + proxy.scheme = newURI.scheme+"://"+newURI.hostPort+"/%p"; + proxy.hosts = [properURI.hostPort]; + } else if(newURI.host != channel.URI.host) { + // New style rewriting + var proxy = new Zotero.Proxy(); + proxy.multiHost = proxy.autoAssociate = true; + proxy.scheme = newURI.scheme+"://"+newURI.hostPort.replace(properURI.host, "%h")+"/%p"; + proxy.hosts = [properURI.hostPort]; + } + return proxy; +} + +/** + * Detector for Juniper Networks WebVPN + */ +Zotero.Proxies.Detectors.Juniper = function(channel) { + const juniperRe = /^(https?:\/\/[^\/:]+(?:\:[0-9]+)?)\/(.*),DanaInfo=([^+,]*)([^+]*)(?:\+(.*))?$/; + try { + var url = channel.URI.spec; + var m = juniperRe.exec(url); + } catch(e) { + return false; + } + if(!m) return false; + + var proxy = new Zotero.Proxy(); + proxy.multiHost = true; + proxy.scheme = m[1]+"/%d"+",DanaInfo=%h%a+%f"; + proxy.hosts = [m[3]]; + return proxy; +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js @@ -1665,6 +1665,12 @@ Zotero.Schema = new function(){ Zotero.DB.query("INSERT INTO itemAttachments (itemID, linkMode) VALUES (?, ?)", [id, 3]); } } + + if (i==39) { + Zotero.DB.query("CREATE TABLE proxies (\n proxyID INTEGER PRIMARY KEY,\n multiHost INT,\n autoAssociate INT,\n scheme TEXT\n)"); + Zotero.DB.query("CREATE TABLE proxyHosts (\n hostID INTEGER PRIMARY KEY,\n proxyID INTEGER,\n hostname TEXT,\n FOREIGN KEY (proxyID) REFERENCES proxies(proxyID)\n)"); + Zotero.DB.query("CREATE INDEX proxyHosts_proxyID ON proxyHosts(proxyID)"); + } } _updateDBVersion('userdata', toVersion); diff --git a/chrome/content/zotero/xpcom/translate.js b/chrome/content/zotero/xpcom/translate.js @@ -271,7 +271,7 @@ Zotero.Translate.prototype.setCollection = function(collection) { Zotero.Translate.prototype.setLocation = function(location) { if(this.type == "web") { // account for proxies - this.location = Zotero.Ingester.ProxyMonitor.proxyToProper(location); + this.location = Zotero.Proxies.proxyToProper(location); if(this.location != location) { // figure out if this URL is being proxies this.locationIsProxied = true; @@ -1109,7 +1109,7 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { // if item was accessed through a proxy, ensure that the proxy // address remains in the accessed version if(this.locationIsProxied && item.url) { - item.url = Zotero.Ingester.ProxyMonitor.properToProxy(item.url); + item.url = Zotero.Proxies.properToProxy(item.url); } // create new item @@ -1383,7 +1383,7 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { } if(this.locationIsProxied) { - attachment.url = Zotero.Ingester.ProxyMonitor.properToProxy(attachment.url); + attachment.url = Zotero.Proxies.properToProxy(attachment.url); } var fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(myID); diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js @@ -555,7 +555,7 @@ Zotero.Utilities.Ingester.prototype.processDocuments = function(urls, processor, if(this.translate.locationIsProxied) { for(var i in urls) { if(this.translate.locationIsProxied) { - urls[i] = Zotero.Ingester.ProxyMonitor.properToProxy(urls[i]); + urls[i] = Zotero.Proxies.properToProxy(urls[i]); } // check for a protocol colon if(!Zotero.Utilities.Ingester._protocolRe.test(urls[i])) { @@ -591,7 +591,7 @@ Zotero.Utilities.Ingester.HTTP.prototype.doGet = function(urls, processor, done, } if(this.translate.locationIsProxied) { - url = Zotero.Ingester.ProxyMonitor.properToProxy(url); + url = Zotero.Proxies.properToProxy(url); } if(!Zotero.Utilities.Ingester._protocolRe.test(url)) { throw("invalid URL in processDocuments"); @@ -620,7 +620,7 @@ Zotero.Utilities.Ingester.HTTP.prototype.doGet = function(urls, processor, done, Zotero.Utilities.Ingester.HTTP.prototype.doPost = function(url, body, onDone, requestContentType, responseCharset) { if(this.translate.locationIsProxied) { - url = Zotero.Ingester.ProxyMonitor.properToProxy(url); + url = Zotero.Ingester.Proxies.properToProxy(url); } if(!Zotero.Utilities.Ingester._protocolRe.test(url)) { throw("invalid URL in processDocuments"); diff --git a/chrome/locale/en-US/zotero/preferences.dtd b/chrome/locale/en-US/zotero/preferences.dtd @@ -61,6 +61,7 @@ <!ENTITY zotero.preferences.quickCopy.siteEditor.outputFormat "Output Format"> <!ENTITY zotero.preferences.prefpane.styles "Styles"> + <!ENTITY zotero.preferences.styles.styleManager "Style Manager"> <!ENTITY zotero.preferences.styles.styleManager.title "Title"> <!ENTITY zotero.preferences.styles.styleManager.updated "Updated"> @@ -81,6 +82,23 @@ <!ENTITY zotero.preferences.keys.overrideGlobal "Try to override conflicting shortcuts"> <!ENTITY zotero.preferences.keys.changesTakeEffect "Changes take effect in new windows only"> +<!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.configured "Configured Proxies"> +<!ENTITY zotero.preferences.proxies.hostname "Hostname"> +<!ENTITY zotero.preferences.proxies.scheme "Scheme"> + +<!ENTITY zotero.preferences.proxies.multiSite "Multi-Site"> +<!ENTITY zotero.preferences.proxies.autoAssociate "Automatically associate new hosts"> +<!ENTITY zotero.preferences.proxies.variables "You may use the following variables in your proxy scheme:"> +<!ENTITY zotero.preferences.proxies.h_variable "%h - The hostname of the proxied site (e.g., www.zotero.org)"> +<!ENTITY zotero.preferences.proxies.p_variable "%p - The path of the proxied page excluding the leading slash (e.g., about/index.html)"> +<!ENTITY zotero.preferences.proxies.d_variable "%d - The directory path (e.g., about/)"> +<!ENTITY zotero.preferences.proxies.f_variable "%f - The filename (e.g., index.html)"> +<!ENTITY zotero.preferences.proxies.a_variable "%a - Any string"> <!ENTITY zotero.preferences.prefpane.advanced "Advanced"> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -498,3 +498,13 @@ styles.updateStyle = Update existing style "%1$S" with "%2$S"? styles.installed = The style "%S" was installed successfully. styles.installError = %S does not appear to be a valid CSL file. styles.deleteStyle = Are you sure you want to delete the style "%1$S"? + +proxies.multiSite = Multi-Site +proxies.error = Information Validation Error +proxies.error.scheme.noHTTP = Valid proxy schemes must start with "http://" or "https://" +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.recognized = Proxy Recognized +proxies.recognized.message = Would you like Zotero to store information about this proxy server to enable saving of references accessed through it?\n\nWARNING: Only click "OK" below if you have accessed this site through your library or another institution you trust. +proxies.recognized.disable = Disable automatic recognition of proxy systems +\ No newline at end of file diff --git a/chrome/skin/default/zotero/prefs-proxies.png b/chrome/skin/default/zotero/prefs-proxies.png Binary files differ. diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -20,7 +20,7 @@ var xpcomFiles = ['zotero', 'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators', 'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'enstyle', 'file', 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime', - 'notifier', 'progressWindow', 'quickCopy', 'report', 'schema', 'search', + 'notifier', 'progressWindow', 'proxy', 'quickCopy', 'report', 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities', 'zeroconf']; for (var i=0; i<xpcomFiles.length; i++) { diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js @@ -79,4 +79,8 @@ pref("extensions.zotero.annotations.warnOnClose", true); // Server pref("extensions.zotero.sync.server.autoSync", true); pref("extensions.zotero.sync.server.username", ''); -pref("extensions.zotero.sync.server.compressData", true); -\ No newline at end of file +pref("extensions.zotero.sync.server.compressData", true); + +// Proxy +pref("extensions.zotero.proxies.autoRecognize", true); +pref("extensions.zotero.proxies.transparent", false); +\ No newline at end of file diff --git a/userdata.sql b/userdata.sql @@ -1,4 +1,4 @@ --- 38 +-- 39 -- This file creates tables containing user-specific data -- any changes made -- here must be mirrored in transition steps in schema.js::_migrateSchema() @@ -254,4 +254,19 @@ CREATE TABLE highlights ( dateModified DATE, FOREIGN KEY (itemID) REFERENCES itemAttachments(itemID) ); -CREATE INDEX highlights_itemID ON highlights(itemID); -\ No newline at end of file +CREATE INDEX highlights_itemID ON highlights(itemID); + +CREATE TABLE proxies ( + proxyID INTEGER PRIMARY KEY, + multiHost INT, + autoAssociate INT, + scheme TEXT +); + +CREATE TABLE proxyHosts ( + hostID INTEGER PRIMARY KEY, + proxyID INTEGER, + hostname TEXT + FOREIGN KEY (proxyID) REFERENCES proxies(proxyID) +); +CREATE INDEX proxyHosts_proxyID ON proxyHosts(proxyID); +\ No newline at end of file