diff --git a/chrome/content/zotero/xpcom/cookieSandbox.js b/chrome/content/zotero/xpcom/cookieSandbox.js new file mode 100755 index 000000000..6682ff93c --- /dev/null +++ b/chrome/content/zotero/xpcom/cookieSandbox.js @@ -0,0 +1,285 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2011 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 . + + ***** END LICENSE BLOCK ***** +*/ + +/** + * Manage cookies in a sandboxed fashion + * + * @constructor + * @param {browser} browser Hidden browser object + * @param {String|nsIURI} uri URI of page to manage cookies for (cookies for domains that are not + * subdomains of this URI are ignored) + * @param {String} cookieData Cookies with which to initiate the sandbox + */ +Zotero.CookieSandbox = function(browser, uri, cookieData) { + this._webNav = browser.webNavigation; + this._browser = browser; + this._watchedBrowsers = [browser]; + this._watchedXHRs = []; + this._observerService = Components.classes["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService); + + if(uri instanceof Components.interfaces.nsIURI) { + this.URI = uri; + } else { + this.URI = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(uri, null, null); + } + + this._cookies = {}; + if(cookieData) { + var splitCookies = cookieData.split(/; ?/); + for each(var cookie in splitCookies) { + var key = cookie.substr(0, cookie.indexOf("=")); + var value = cookie.substr(cookie.indexOf("=")+1); + this._cookies[key] = value; + } + } + + // register with observer + Zotero.CookieSandbox.Observer.register(this); +} + +Zotero.CookieSandbox.prototype = { + /** + * Check whether we track a browser for this document + */ + "isDocumentTracked":function(doc) { + var i = this._watchedBrowsers.length; + while(i--) { + var browser = this._watchedBrowsers[i]; + if(doc == browser.contentDocument) return true; + } + return false; + }, + + /** + * Check whether we track an XHR for this document + */ + "isXHRTracked":function(xhr) { + return this._watchedXHRs.indexOf(xhr) !== -1; + }, + + /** + * Adds cookies to this CookieSandbox based on a cookie header + * @param {String} cookieString; + */ + "addCookiesFromHeader":function(cookieString) { + var cookies = cookieString.split("\n"); + for(var i=0, n=cookies.length; i we should manage cookies for this request + // tested && !trackedBy => we should not manage cookies for this request + // !tested && !trackedBy => this request is of a type we couldn't match to this request. + // one such type is a link prefetch (nsPrefetchNode) but there might be others as + // well. for now, we are paranoid and reject these. + + if(tested) { + if(trackedBy) { + Zotero.debug("CookieSandbox: Managing cookies for "+channelURI, 5); + } else { + Zotero.debug("CookieSandbox: Not touching channel for "+channelURI, 5); + return; + } + } else { + Zotero.debug("CookieSandbox: Being paranoid about channel for "+channelURI, 5); + } + + if(topic == "http-on-modify-request") { + // clear cookies to be sent to other domains + if(!trackedBy || channel.URI.host != trackedBy.URI.host) { + channel.setRequestHeader("Cookie", "", false); + channel.setRequestHeader("Cookie2", "", false); + Zotero.debug("CookieSandbox: Cleared cookies to be sent to "+channelURI, 5); + return; + } + + // add cookies to be sent to this domain + Zotero.debug(trackedBy.cookieString); + channel.setRequestHeader("Cookie", trackedBy.cookieString, false); + Zotero.debug("CookieSandbox: Added cookies for request to "+channelURI, 5); + } else if(topic == "http-on-examine-response") { + // clear cookies being received + try { + var cookieHeader = channel.getResponseHeader("Set-Cookie"); + } catch(e) { + return; + } + channel.setResponseHeader("Set-Cookie", "", false); + channel.setResponseHeader("Set-Cookie2", "", false); + + // don't process further if these cookies are for another set of domains + if(!trackedBy || channel.URI.host != trackedBy.URI.host) { + Zotero.debug("CookieSandbox: Rejected cookies from "+channelURI, 5); + return; + } + + // put new cookies into our sandbox + Zotero.debug(cookieHeader); + if(cookieHeader) trackedBy.addCookiesFromHeader(cookieHeader); + + Zotero.debug("CookieSandbox: Slurped cookies from "+channelURI, 5); + } + } +} \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index a420d133b..2e3e9aca0 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -12,9 +12,10 @@ Zotero.HTTP = new function() { * @param {nsIURI|String} url URL to request * @param {Function} onDone Callback to be executed upon request completion * @param {String} responseCharset Character set to force on the response + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object * @return {Boolean} True if the request was sent, or false if the browser is offline */ - this.doGet = function(url, onDone, responseCharset) { + this.doGet = function(url, onDone, responseCharset, cookieSandbox) { if (url instanceof Components.interfaces.nsIURI) { // Don't display password in console var disp = url.clone(); @@ -69,6 +70,7 @@ Zotero.HTTP = new function() { _stateChange(xmlhttp, onDone, responseCharset); }; + if(cookieSandbox) cookieSandbox.attachToXHR(xmlhttp); xmlhttp.send(null); return xmlhttp; @@ -82,9 +84,10 @@ Zotero.HTTP = new function() { * @param {Function} onDone Callback to be executed upon request completion * @param {String} headers Request HTTP headers * @param {String} responseCharset Character set to force on the response + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object * @return {Boolean} True if the request was sent, or false if the browser is offline */ - this.doPost = function(url, body, onDone, headers, responseCharset) { + this.doPost = function(url, body, onDone, headers, responseCharset, cookieSandbox) { if (url instanceof Components.interfaces.nsIURI) { // Don't display password in console var disp = url.clone(); @@ -166,6 +169,7 @@ Zotero.HTTP = new function() { _stateChange(xmlhttp, onDone, responseCharset); }; + if(cookieSandbox) cookieSandbox.attachToXHR(xmlhttp); xmlhttp.send(body); return xmlhttp; @@ -506,9 +510,10 @@ Zotero.HTTP = new function() { * @param {Function} exception Callback to be executed if an exception occurs * @param {Boolean} dontDelete Don't delete the hidden browser upon completion; calling function * must call deleteHiddenBrowser itself. + * @param {Zotero.CookieSandbox} [cookieSandbox] Cookie sandbox object * @return {browser} Hidden browser used for loading */ - this.processDocuments = function(urls, processor, done, exception, dontDelete) { + this.processDocuments = function(urls, processor, done, exception, dontDelete, cookieSandbox) { /** * Removes event listener for the load event and deletes the hidden browser */ @@ -573,6 +578,7 @@ Zotero.HTTP = new function() { var hiddenBrowser = Zotero.Browser.createHiddenBrowser(); hiddenBrowser.addEventListener(loadEvent, onLoad, true); + if(cookieSandbox) cookieSandbox.attachToBrowser(hiddenBrowser); doLoad(); diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js index e4d068e25..fd90687c1 100755 --- a/chrome/content/zotero/xpcom/server_connector.js +++ b/chrome/content/zotero/xpcom/server_connector.js @@ -29,169 +29,6 @@ Zotero.Server.Connector = function() {}; Zotero.Server.Connector._waitingForSelection = {}; Zotero.Server.Connector.Data = {}; -/** - * Manage cookies in a sandboxed fashion - * - * @param {browser} browser Hidden browser object - * @param {String} uri URI of page to manage cookies for (cookies for domains that are not - * subdomains of this URI are ignored) - * @param {String} cookieData Cookies with which to initiate the sandbox - */ -Zotero.Server.Connector.CookieManager = function(browser, uri, cookieData) { - this._webNav = browser.webNavigation; - this._browser = browser; - this._watchedBrowsers = [browser]; - this._observerService = Components.classes["@mozilla.org/observer-service;1"]. - getService(Components.interfaces.nsIObserverService); - - this._uri = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService) - .newURI(uri, null, null); - - var splitCookies = cookieData.split(/; ?/); - this._cookies = {}; - for each(var cookie in splitCookies) { - var key = cookie.substr(0, cookie.indexOf("=")); - var value = cookie.substr(cookie.indexOf("=")+1); - this._cookies[key] = value; - } - - [this._observerService.addObserver(this, topic, false) for each(topic in this._observerTopics)]; -} - -Zotero.Server.Connector.CookieManager.prototype = { - "_observerTopics":["http-on-examine-response", "http-on-modify-request", "quit-application"], - "_watchedXHRs":[], - - /** - * nsIObserver implementation for adding, clearing, and slurping cookies - */ - "observe": function(channel, topic) { - if(topic == "quit-application") { - Zotero.debug("WARNING: A Zotero.Server.CookieManager for "+this._uri.spec+" was still open on shutdown"); - } else { - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - var isTracked = null; - try { - var topDoc = channel.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document; - for each(var browser in this._watchedBrowsers) { - isTracked = topDoc == browser.contentDocument; - if(isTracked) break; - } - } catch(e) {} - if(isTracked === null) { - try { - isTracked = channel.loadGroup.notificationCallbacks.getInterface(Components.interfaces.nsIDOMWindow).top.document == this._browser.contentDocument; - } catch(e) {} - } - if(isTracked === null) { - try { - isTracked = this._watchedXHRs.indexOf(channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIXMLHttpRequest)) !== -1; - } catch(e) {} - } - - // isTracked is now either true, false, or null - // true => we should manage cookies for this request - // false => we should not manage cookies for this request - // null => this request is of a type we couldn't match to this request. one such type - // is a link prefetch (nsPrefetchNode) but there might be others as well. for - // now, we are paranoid and reject these. - - if(isTracked === false) { - Zotero.debug("Zotero.Server.CookieManager: not touching channel for "+channel.URI.spec); - return; - } else if(isTracked) { - Zotero.debug("Zotero.Server.CookieManager: managing cookies for "+channel.URI.spec); - } else { - Zotero.debug("Zotero.Server.CookieManager: being paranoid about channel for "+channel.URI.spec); - } - - if(topic == "http-on-modify-request") { - // clear cookies to be sent to other domains - if(isTracked === null || channel.URI.host != this._uri.host) { - channel.setRequestHeader("Cookie", "", false); - channel.setRequestHeader("Cookie2", "", false); - Zotero.debug("Zotero.Server.CookieManager: cleared cookies to be sent to "+channel.URI.spec); - return; - } - - // add cookies to be sent to this domain - var cookies = [key+"="+this._cookies[key] - for(key in this._cookies)].join("; "); - channel.setRequestHeader("Cookie", cookies, false); - Zotero.debug("Zotero.Server.CookieManager: added cookies for request to "+channel.URI.spec); - } else if(topic == "http-on-examine-response") { - // clear cookies being received - try { - var cookieHeader = channel.getResponseHeader("Set-Cookie"); - } catch(e) { - return; - } - channel.setResponseHeader("Set-Cookie", "", false); - channel.setResponseHeader("Set-Cookie2", "", false); - - // don't process further if these cookies are for another set of domains - if(isTracked === null || channel.URI.host != this._uri.host) { - Zotero.debug("Zotero.Server.CookieManager: rejected cookies from "+channel.URI.spec); - return; - } - - // put new cookies into our sandbox - if(cookieHeader) { - var cookies = cookieHeader.split(/; ?/); - var newCookies = {}; - for each(var cookie in cookies) { - var key = cookie.substr(0, cookie.indexOf("=")); - var value = cookie.substr(cookie.indexOf("=")+1); - var lcCookie = key.toLowerCase(); - - if(["comment", "domain", "max-age", "path", "version", "expires"].indexOf(lcCookie) != -1) { - // ignore cookie parameters; we are only holding cookies for a few minutes - // with a single domain, and the path attribute doesn't allow any additional - // security. - // DEBUG: does ignoring the path attribute break any sites? - continue; - } else if(lcCookie == "secure") { - // don't accept secure cookies - newCookies = {}; - break; - } else { - newCookies[key] = value; - } - } - [this._cookies[key] = newCookies[key] for(key in newCookies)]; - } - - Zotero.debug("Zotero.Server.CookieManager: slurped cookies from "+channel.URI.spec); - } - } - }, - - /** - * Attach CookieManager to a specific XMLHttpRequest - * @param {XMLHttpRequest} xhr - */ - "attachToBrowser": function(browser) { - this._watchedBrowsers.push(browser); - }, - - /** - * Attach CookieManager to a specific XMLHttpRequest - * @param {XMLHttpRequest} xhr - */ - "attachToXHR": function(xhr) { - this._watchedXHRs.push(xhr); - }, - - /** - * Destroys this CookieManager (intended to be executed when the browser is destroyed) - */ - "destroy": function() { - [this._observerService.removeObserver(this, topic) for each(topic in this._observerTopics)]; - } -} - - /** * Lists all available translators, including code for translators that should be run on every page * @@ -268,7 +105,7 @@ Zotero.Server.Connector.Detect.prototype = { var pageShowCalled = false; var me = this; - this._translate.setCookieManager(new Zotero.Server.Connector.CookieManager(this._browser, + this._translate.setCookieSandbox(new Zotero.CookieSandbox(this._browser, this._parsedPostData["uri"], this._parsedPostData["cookie"])); this._browser.addEventListener("DOMContentLoaded", function() { try { @@ -310,7 +147,7 @@ Zotero.Server.Connector.Detect.prototype = { } this._sendResponse(200, "application/json", JSON.stringify(jsons)); - this._translate.cookieManager.destroy(); + this._translate.cookieSandbox.destroy(); Zotero.Browser.deleteHiddenBrowser(this._browser); } } @@ -396,6 +233,7 @@ Zotero.Server.Connector.SavePage.prototype = { "_translatorsAvailable":function(translate, translators) { // make sure translatorsAvailable succeded if(!translators.length) { + me._translate.cookieSandbox.destroy(); Zotero.Browser.deleteHiddenBrowser(this._browser); this._sendResponse(500); return; @@ -420,10 +258,14 @@ Zotero.Server.Connector.SavePage.prototype = { } jsonItems.push(jsonItem); }); - translate.setHandler("done", function(obj, item) { - me._translate.cookieManager.destroy(); + translate.setHandler("done", function(obj, item) { + me._translate.cookieSandbox.destroy(); Zotero.Browser.deleteHiddenBrowser(me._browser); - me._sendResponse(201, "application/json", JSON.stringify({"items":jsonItems})); + if(jsonItems.length) { + me._sendResponse(201, "application/json", JSON.stringify({"items":jsonItems})); + } else { + me._sendResponse(500); + } }); // set translator and translate diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js index 397b246d7..c55837dbb 100644 --- a/chrome/content/zotero/xpcom/translation/translate.js +++ b/chrome/content/zotero/xpcom/translation/translate.js @@ -1315,7 +1315,7 @@ Zotero.Translate.Base.prototype = { * @class Web translation * * @property {Document} document The document object to be used for web scraping (set with setDocument) - * @property {Zotero.Connector.CookieManager} cookieManager A CookieManager to manage cookies for + * @property {Zotero.CookieSandbox} cookieSandbox A CookieSandbox to manage cookies for * this Translate instance. */ Zotero.Translate.Web = function() { @@ -1336,13 +1336,13 @@ Zotero.Translate.Web.prototype.setDocument = function(doc) { } /** - * Sets a Zotero.Connector.CookieManager to handle cookie management for XHRs initiated from this + * Sets a Zotero.CookieSandbox to handle cookie management for XHRs initiated from this * translate instance * - * @param {Zotero.Connector.CookieManager} cookieManager + * @param {Zotero.CookieSandbox} cookieSandbox */ -Zotero.Translate.Web.prototype.setCookieManager = function(cookieManager) { - this.cookieManager = cookieManager; +Zotero.Translate.Web.prototype.setCookieSandbox = function(cookieSandbox) { + this.cookieSandbox = cookieSandbox; } /** @@ -1757,9 +1757,9 @@ Zotero.Translate.Search.prototype._entryFunctionSuffix = "Search"; Zotero.Translate.Search.prototype.Sandbox = Zotero.Translate.Sandbox._inheritFromBase(Zotero.Translate.Sandbox.Search); /** - * @borrows Zotero.Translate.Web#setCookieManager + * @borrows Zotero.Translate.Web#setCookieSandbox */ -Zotero.Translate.Search.prototype.setCookieManager = Zotero.Translate.Web.prototype.setCookieManager; +Zotero.Translate.Search.prototype.setCookieSandbox = Zotero.Translate.Web.prototype.setCookieSandbox; /** * Sets the item to be used for searching diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index 6a9613a26..4e2920b5e 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -1155,10 +1155,10 @@ Zotero.Utilities.Translate.prototype.processDocuments = function(urls, processor var translate = this._translate; translate.incrementAsyncProcesses(); - Zotero.HTTP.processDocuments(urls, processor, function() { + var hiddenBrowser = Zotero.HTTP.processDocuments(urls, processor, function() { if(done) done(); translate.decrementAsyncProcesses(); - }, exception); + }, exception, false, translate.cookieSandbox); } /** @@ -1181,6 +1181,8 @@ Zotero.Utilities.Translate.prototype.retrieveDocument = function(url) { } var hiddenBrowser = Zotero.Browser.createHiddenBrowser(); + if(translate.cookieSandbox) translate.cookieSandbox.attachToBrowser(hiddenBrowser); + hiddenBrowser.addEventListener("pageshow", listener, true); hiddenBrowser.loadURI(url); @@ -1228,16 +1230,16 @@ Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, header if(!responseCharset) responseCharset = null; var mainThread = Zotero.mainThread; - var xmlhttp = false; - var listener = function(aXmlhttp) { xmlhttp = aXmlhttp }; + var finished = false; + var listener = function() { finished = true }; if(body) { - Zotero.HTTP.doPost(url, body, listener, headers, responseCharset); + var xmlhttp = Zotero.HTTP.doPost(url, body, listener, headers, responseCharset, translate.cookieSandbox); } else { - Zotero.HTTP.doGet(url, listener, responseCharset); + var xmlhttp = Zotero.HTTP.doGet(url, listener, responseCharset, translate.cookieSandbox); } - while(!xmlhttp) mainThread.processNextEvent(true); + while(!finished) mainThread.processNextEvent(true); } else { // Use a synchronous XMLHttpRequest, even though this is inadvisable var xmlhttp = new XMLHttpRequest(); @@ -1274,7 +1276,7 @@ Zotero.Utilities.Translate.prototype.doGet = function(urls, processor, done, res var me = this; this._translate.incrementAsyncProcesses(); - Zotero.HTTP.doGet(url, function(xmlhttp) { + var xmlhttp = Zotero.HTTP.doGet(url, function(xmlhttp) { try { if(processor) { processor(xmlhttp.responseText, xmlhttp, url); @@ -1291,7 +1293,7 @@ Zotero.Utilities.Translate.prototype.doGet = function(urls, processor, done, res } catch(e) { me._translate.complete(false, e); } - }, responseCharset); + }, responseCharset, this._translate.cookieSandbox); } /** @@ -1303,14 +1305,14 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, header var translate = this._translate; this._translate.incrementAsyncProcesses(); - Zotero.HTTP.doPost(url, body, function(xmlhttp) { + var xmlhttp = Zotero.HTTP.doPost(url, body, function(xmlhttp) { try { onDone(xmlhttp.responseText, xmlhttp); translate.decrementAsyncProcesses(); } catch(e) { translate.complete(false, e); } - }, headers, responseCharset); + }, headers, responseCharset, translate.cookieSandbox ? translate.cookieSandbox : undefined); } /** diff --git a/components/zotero-service.js b/components/zotero-service.js index c05cf00c8..08a3e3e8b 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -55,6 +55,7 @@ const xpcomFilesLocal = [ 'attachments', 'cite', 'commons', + 'cookieSandbox', 'data_access', 'data/dataObjects', 'data/cachedTypes',