michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["SessionCookies"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm", this); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Utils", michael@0: "resource:///modules/sessionstore/Utils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", michael@0: "resource:///modules/sessionstore/PrivacyLevel.jsm"); michael@0: michael@0: // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. michael@0: const MAX_EXPIRY = Math.pow(2, 62); michael@0: michael@0: /** michael@0: * The external API implemented by the SessionCookies module. michael@0: */ michael@0: this.SessionCookies = Object.freeze({ michael@0: update: function (windows) { michael@0: SessionCookiesInternal.update(windows); michael@0: }, michael@0: michael@0: getHostsForWindow: function (window, checkPrivacy = false) { michael@0: return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * The internal API. michael@0: */ michael@0: let SessionCookiesInternal = { michael@0: /** michael@0: * Stores whether we're initialized, yet. michael@0: */ michael@0: _initialized: false, michael@0: michael@0: /** michael@0: * Retrieve the list of all hosts contained in the given windows' session michael@0: * history entries (per window) and collect the associated cookies for those michael@0: * hosts, if any. The given state object is being modified. michael@0: * michael@0: * @param windows michael@0: * Array of window state objects. michael@0: * [{ tabs: [...], cookies: [...] }, ...] michael@0: */ michael@0: update: function (windows) { michael@0: this._ensureInitialized(); michael@0: michael@0: for (let window of windows) { michael@0: let cookies = []; michael@0: michael@0: // Collect all hosts for the current window. michael@0: let hosts = this.getHostsForWindow(window, true); michael@0: michael@0: for (let host of Object.keys(hosts)) { michael@0: let isPinned = hosts[host]; michael@0: michael@0: for (let cookie of CookieStore.getCookiesForHost(host)) { michael@0: // _getCookiesForHost() will only return hosts with the right privacy michael@0: // rules, so there is no need to do anything special with this call michael@0: // to PrivacyLevel.canSave(). michael@0: if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) { michael@0: cookies.push(cookie); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Don't include/keep empty cookie sections. michael@0: if (cookies.length) { michael@0: window.cookies = cookies; michael@0: } else if ("cookies" in window) { michael@0: delete window.cookies; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns a map of all hosts for a given window that we might want to michael@0: * collect cookies for. michael@0: * michael@0: * @param window michael@0: * A window state object containing tabs with history entries. michael@0: * @param checkPrivacy (bool) michael@0: * Whether to check the privacy level for each host. michael@0: * @return {object} A map of hosts for a given window state object. The keys michael@0: * will be hosts, the values are boolean and determine michael@0: * whether we will use the deferred privacy level when michael@0: * checking how much data to save on quitting. michael@0: */ michael@0: getHostsForWindow: function (window, checkPrivacy = false) { michael@0: let hosts = {}; michael@0: michael@0: for (let tab of window.tabs) { michael@0: for (let entry of tab.entries) { michael@0: this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned); michael@0: } michael@0: } michael@0: michael@0: return hosts; michael@0: }, michael@0: michael@0: /** michael@0: * Handles observers notifications that are sent whenever cookies are added, michael@0: * changed, or removed. Ensures that the storage is updated accordingly. michael@0: */ michael@0: observe: function (subject, topic, data) { michael@0: switch (data) { michael@0: case "added": michael@0: case "changed": michael@0: this._updateCookie(subject); michael@0: break; michael@0: case "deleted": michael@0: this._removeCookie(subject); michael@0: break; michael@0: case "cleared": michael@0: CookieStore.clear(); michael@0: break; michael@0: case "batch-deleted": michael@0: this._removeCookies(subject); michael@0: break; michael@0: case "reload": michael@0: CookieStore.clear(); michael@0: this._reloadCookies(); michael@0: break; michael@0: default: michael@0: throw new Error("Unhandled cookie-changed notification."); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * If called for the first time in a session, iterates all cookies in the michael@0: * cookies service and puts them into the store if they're session cookies. michael@0: */ michael@0: _ensureInitialized: function () { michael@0: if (!this._initialized) { michael@0: this._reloadCookies(); michael@0: this._initialized = true; michael@0: Services.obs.addObserver(this, "cookie-changed", false); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Fill a given map with hosts found in the given entry's session history and michael@0: * any child entries. michael@0: * michael@0: * @param entry michael@0: * the history entry, serialized michael@0: * @param hosts michael@0: * the hash that will be used to store hosts eg, { hostname: true } michael@0: * @param checkPrivacy michael@0: * should we check the privacy level for https michael@0: * @param isPinned michael@0: * is the entry we're evaluating for a pinned tab; used only if michael@0: * checkPrivacy michael@0: */ michael@0: _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) { michael@0: let host = entry._host; michael@0: let scheme = entry._scheme; michael@0: michael@0: // If host & scheme aren't defined, then we are likely here in the startup michael@0: // process via _splitCookiesFromWindow. In that case, we'll turn entry.url michael@0: // into an nsIURI and get host/scheme from that. This will throw for about: michael@0: // urls in which case we don't need to do anything. michael@0: if (!host && !scheme) { michael@0: try { michael@0: let uri = Utils.makeURI(entry.url); michael@0: host = uri.host; michael@0: scheme = uri.scheme; michael@0: this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned); michael@0: } michael@0: catch (ex) { } michael@0: } michael@0: michael@0: if (entry.children) { michael@0: for (let child of entry.children) { michael@0: this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add a given host to a given map of hosts if the privacy level allows michael@0: * saving cookie data for it. michael@0: * michael@0: * @param host michael@0: * the host of a uri (usually via nsIURI.host) michael@0: * @param scheme michael@0: * the scheme of a uri (usually via nsIURI.scheme) michael@0: * @param hosts michael@0: * the hash that will be used to store hosts eg, { hostname: true } michael@0: * @param checkPrivacy michael@0: * should we check the privacy level for https michael@0: * @param isPinned michael@0: * is the entry we're evaluating for a pinned tab; used only if michael@0: * checkPrivacy michael@0: */ michael@0: _extractHostsFromHostScheme: michael@0: function (host, scheme, hosts, checkPrivacy, isPinned) { michael@0: // host and scheme may not be set (for about: urls for example), in which michael@0: // case testing scheme will be sufficient. michael@0: if (/https?/.test(scheme) && !hosts[host] && michael@0: (!checkPrivacy || michael@0: PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) { michael@0: // By setting this to true or false, we can determine when looking at michael@0: // the host in update() if we should check for privacy. michael@0: hosts[host] = isPinned; michael@0: } else if (scheme == "file") { michael@0: hosts[host] = true; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Updates or adds a given cookie to the store. michael@0: */ michael@0: _updateCookie: function (cookie) { michael@0: cookie.QueryInterface(Ci.nsICookie2); michael@0: michael@0: if (cookie.isSession) { michael@0: CookieStore.set(cookie); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes a given cookie from the store. michael@0: */ michael@0: _removeCookie: function (cookie) { michael@0: cookie.QueryInterface(Ci.nsICookie2); michael@0: michael@0: if (cookie.isSession) { michael@0: CookieStore.delete(cookie); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes a given list of cookies from the store. michael@0: */ michael@0: _removeCookies: function (cookies) { michael@0: for (let i = 0; i < cookies.length; i++) { michael@0: this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2)); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Iterates all cookies in the cookies service and puts them into the store michael@0: * if they're session cookies. michael@0: */ michael@0: _reloadCookies: function () { michael@0: let iter = Services.cookies.enumerator; michael@0: while (iter.hasMoreElements()) { michael@0: this._updateCookie(iter.getNext()); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * The internal cookie storage that keeps track of every active session cookie. michael@0: * These are stored using maps per host, path, and cookie name. michael@0: */ michael@0: let CookieStore = { michael@0: /** michael@0: * The internal structure holding all known cookies. michael@0: * michael@0: * Host => michael@0: * Path => michael@0: * Name => {path: "/", name: "sessionid", secure: true} michael@0: * michael@0: * Maps are used for storage but the data structure is equivalent to this: michael@0: * michael@0: * this._hosts = { michael@0: * "www.mozilla.org": { michael@0: * "/": { michael@0: * "username": {name: "username", value: "my_name_is", etc...}, michael@0: * "sessionid": {name: "sessionid", value: "1fdb3a", etc...} michael@0: * } michael@0: * }, michael@0: * "tbpl.mozilla.org": { michael@0: * "/path": { michael@0: * "cookiename": {name: "cookiename", value: "value", etc...} michael@0: * } michael@0: * } michael@0: * }; michael@0: */ michael@0: _hosts: new Map(), michael@0: michael@0: /** michael@0: * Returns the list of stored session cookies for a given host. michael@0: * michael@0: * @param host michael@0: * A string containing the host name we want to get cookies for. michael@0: */ michael@0: getCookiesForHost: function (host) { michael@0: if (!this._hosts.has(host)) { michael@0: return []; michael@0: } michael@0: michael@0: let cookies = []; michael@0: michael@0: for (let pathToNamesMap of this._hosts.get(host).values()) { michael@0: cookies.push(...pathToNamesMap.values()); michael@0: } michael@0: michael@0: return cookies; michael@0: }, michael@0: michael@0: /** michael@0: * Stores a given cookie. michael@0: * michael@0: * @param cookie michael@0: * The nsICookie2 object to add to the storage. michael@0: */ michael@0: set: function (cookie) { michael@0: let jscookie = {host: cookie.host, value: cookie.value}; michael@0: michael@0: // Only add properties with non-default values to save a few bytes. michael@0: if (cookie.path) { michael@0: jscookie.path = cookie.path; michael@0: } michael@0: michael@0: if (cookie.name) { michael@0: jscookie.name = cookie.name; michael@0: } michael@0: michael@0: if (cookie.isSecure) { michael@0: jscookie.secure = true; michael@0: } michael@0: michael@0: if (cookie.isHttpOnly) { michael@0: jscookie.httponly = true; michael@0: } michael@0: michael@0: if (cookie.expiry < MAX_EXPIRY) { michael@0: jscookie.expiry = cookie.expiry; michael@0: } michael@0: michael@0: this._ensureMap(cookie).set(cookie.name, jscookie); michael@0: }, michael@0: michael@0: /** michael@0: * Removes a given cookie. michael@0: * michael@0: * @param cookie michael@0: * The nsICookie2 object to be removed from storage. michael@0: */ michael@0: delete: function (cookie) { michael@0: this._ensureMap(cookie).delete(cookie.name); michael@0: }, michael@0: michael@0: /** michael@0: * Removes all cookies. michael@0: */ michael@0: clear: function () { michael@0: this._hosts.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Creates all maps necessary to store a given cookie. michael@0: * michael@0: * @param cookie michael@0: * The nsICookie2 object to create maps for. michael@0: * michael@0: * @return The newly created Map instance mapping cookie names to michael@0: * internal jscookies, in the given path of the given host. michael@0: */ michael@0: _ensureMap: function (cookie) { michael@0: if (!this._hosts.has(cookie.host)) { michael@0: this._hosts.set(cookie.host, new Map()); michael@0: } michael@0: michael@0: let pathToNamesMap = this._hosts.get(cookie.host); michael@0: michael@0: if (!pathToNamesMap.has(cookie.path)) { michael@0: pathToNamesMap.set(cookie.path, new Map()); michael@0: } michael@0: michael@0: return pathToNamesMap.get(cookie.path); michael@0: } michael@0: };