1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/src/SessionCookies.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,385 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["SessionCookies"]; 1.11 + 1.12 +const Cu = Components.utils; 1.13 +const Ci = Components.interfaces; 1.14 + 1.15 +Cu.import("resource://gre/modules/Services.jsm", this); 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); 1.17 + 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "Utils", 1.19 + "resource:///modules/sessionstore/Utils.jsm"); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", 1.21 + "resource:///modules/sessionstore/PrivacyLevel.jsm"); 1.22 + 1.23 +// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. 1.24 +const MAX_EXPIRY = Math.pow(2, 62); 1.25 + 1.26 +/** 1.27 + * The external API implemented by the SessionCookies module. 1.28 + */ 1.29 +this.SessionCookies = Object.freeze({ 1.30 + update: function (windows) { 1.31 + SessionCookiesInternal.update(windows); 1.32 + }, 1.33 + 1.34 + getHostsForWindow: function (window, checkPrivacy = false) { 1.35 + return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy); 1.36 + } 1.37 +}); 1.38 + 1.39 +/** 1.40 + * The internal API. 1.41 + */ 1.42 +let SessionCookiesInternal = { 1.43 + /** 1.44 + * Stores whether we're initialized, yet. 1.45 + */ 1.46 + _initialized: false, 1.47 + 1.48 + /** 1.49 + * Retrieve the list of all hosts contained in the given windows' session 1.50 + * history entries (per window) and collect the associated cookies for those 1.51 + * hosts, if any. The given state object is being modified. 1.52 + * 1.53 + * @param windows 1.54 + * Array of window state objects. 1.55 + * [{ tabs: [...], cookies: [...] }, ...] 1.56 + */ 1.57 + update: function (windows) { 1.58 + this._ensureInitialized(); 1.59 + 1.60 + for (let window of windows) { 1.61 + let cookies = []; 1.62 + 1.63 + // Collect all hosts for the current window. 1.64 + let hosts = this.getHostsForWindow(window, true); 1.65 + 1.66 + for (let host of Object.keys(hosts)) { 1.67 + let isPinned = hosts[host]; 1.68 + 1.69 + for (let cookie of CookieStore.getCookiesForHost(host)) { 1.70 + // _getCookiesForHost() will only return hosts with the right privacy 1.71 + // rules, so there is no need to do anything special with this call 1.72 + // to PrivacyLevel.canSave(). 1.73 + if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) { 1.74 + cookies.push(cookie); 1.75 + } 1.76 + } 1.77 + } 1.78 + 1.79 + // Don't include/keep empty cookie sections. 1.80 + if (cookies.length) { 1.81 + window.cookies = cookies; 1.82 + } else if ("cookies" in window) { 1.83 + delete window.cookies; 1.84 + } 1.85 + } 1.86 + }, 1.87 + 1.88 + /** 1.89 + * Returns a map of all hosts for a given window that we might want to 1.90 + * collect cookies for. 1.91 + * 1.92 + * @param window 1.93 + * A window state object containing tabs with history entries. 1.94 + * @param checkPrivacy (bool) 1.95 + * Whether to check the privacy level for each host. 1.96 + * @return {object} A map of hosts for a given window state object. The keys 1.97 + * will be hosts, the values are boolean and determine 1.98 + * whether we will use the deferred privacy level when 1.99 + * checking how much data to save on quitting. 1.100 + */ 1.101 + getHostsForWindow: function (window, checkPrivacy = false) { 1.102 + let hosts = {}; 1.103 + 1.104 + for (let tab of window.tabs) { 1.105 + for (let entry of tab.entries) { 1.106 + this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned); 1.107 + } 1.108 + } 1.109 + 1.110 + return hosts; 1.111 + }, 1.112 + 1.113 + /** 1.114 + * Handles observers notifications that are sent whenever cookies are added, 1.115 + * changed, or removed. Ensures that the storage is updated accordingly. 1.116 + */ 1.117 + observe: function (subject, topic, data) { 1.118 + switch (data) { 1.119 + case "added": 1.120 + case "changed": 1.121 + this._updateCookie(subject); 1.122 + break; 1.123 + case "deleted": 1.124 + this._removeCookie(subject); 1.125 + break; 1.126 + case "cleared": 1.127 + CookieStore.clear(); 1.128 + break; 1.129 + case "batch-deleted": 1.130 + this._removeCookies(subject); 1.131 + break; 1.132 + case "reload": 1.133 + CookieStore.clear(); 1.134 + this._reloadCookies(); 1.135 + break; 1.136 + default: 1.137 + throw new Error("Unhandled cookie-changed notification."); 1.138 + } 1.139 + }, 1.140 + 1.141 + /** 1.142 + * If called for the first time in a session, iterates all cookies in the 1.143 + * cookies service and puts them into the store if they're session cookies. 1.144 + */ 1.145 + _ensureInitialized: function () { 1.146 + if (!this._initialized) { 1.147 + this._reloadCookies(); 1.148 + this._initialized = true; 1.149 + Services.obs.addObserver(this, "cookie-changed", false); 1.150 + } 1.151 + }, 1.152 + 1.153 + /** 1.154 + * Fill a given map with hosts found in the given entry's session history and 1.155 + * any child entries. 1.156 + * 1.157 + * @param entry 1.158 + * the history entry, serialized 1.159 + * @param hosts 1.160 + * the hash that will be used to store hosts eg, { hostname: true } 1.161 + * @param checkPrivacy 1.162 + * should we check the privacy level for https 1.163 + * @param isPinned 1.164 + * is the entry we're evaluating for a pinned tab; used only if 1.165 + * checkPrivacy 1.166 + */ 1.167 + _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) { 1.168 + let host = entry._host; 1.169 + let scheme = entry._scheme; 1.170 + 1.171 + // If host & scheme aren't defined, then we are likely here in the startup 1.172 + // process via _splitCookiesFromWindow. In that case, we'll turn entry.url 1.173 + // into an nsIURI and get host/scheme from that. This will throw for about: 1.174 + // urls in which case we don't need to do anything. 1.175 + if (!host && !scheme) { 1.176 + try { 1.177 + let uri = Utils.makeURI(entry.url); 1.178 + host = uri.host; 1.179 + scheme = uri.scheme; 1.180 + this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned); 1.181 + } 1.182 + catch (ex) { } 1.183 + } 1.184 + 1.185 + if (entry.children) { 1.186 + for (let child of entry.children) { 1.187 + this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned); 1.188 + } 1.189 + } 1.190 + }, 1.191 + 1.192 + /** 1.193 + * Add a given host to a given map of hosts if the privacy level allows 1.194 + * saving cookie data for it. 1.195 + * 1.196 + * @param host 1.197 + * the host of a uri (usually via nsIURI.host) 1.198 + * @param scheme 1.199 + * the scheme of a uri (usually via nsIURI.scheme) 1.200 + * @param hosts 1.201 + * the hash that will be used to store hosts eg, { hostname: true } 1.202 + * @param checkPrivacy 1.203 + * should we check the privacy level for https 1.204 + * @param isPinned 1.205 + * is the entry we're evaluating for a pinned tab; used only if 1.206 + * checkPrivacy 1.207 + */ 1.208 + _extractHostsFromHostScheme: 1.209 + function (host, scheme, hosts, checkPrivacy, isPinned) { 1.210 + // host and scheme may not be set (for about: urls for example), in which 1.211 + // case testing scheme will be sufficient. 1.212 + if (/https?/.test(scheme) && !hosts[host] && 1.213 + (!checkPrivacy || 1.214 + PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) { 1.215 + // By setting this to true or false, we can determine when looking at 1.216 + // the host in update() if we should check for privacy. 1.217 + hosts[host] = isPinned; 1.218 + } else if (scheme == "file") { 1.219 + hosts[host] = true; 1.220 + } 1.221 + }, 1.222 + 1.223 + /** 1.224 + * Updates or adds a given cookie to the store. 1.225 + */ 1.226 + _updateCookie: function (cookie) { 1.227 + cookie.QueryInterface(Ci.nsICookie2); 1.228 + 1.229 + if (cookie.isSession) { 1.230 + CookieStore.set(cookie); 1.231 + } 1.232 + }, 1.233 + 1.234 + /** 1.235 + * Removes a given cookie from the store. 1.236 + */ 1.237 + _removeCookie: function (cookie) { 1.238 + cookie.QueryInterface(Ci.nsICookie2); 1.239 + 1.240 + if (cookie.isSession) { 1.241 + CookieStore.delete(cookie); 1.242 + } 1.243 + }, 1.244 + 1.245 + /** 1.246 + * Removes a given list of cookies from the store. 1.247 + */ 1.248 + _removeCookies: function (cookies) { 1.249 + for (let i = 0; i < cookies.length; i++) { 1.250 + this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2)); 1.251 + } 1.252 + }, 1.253 + 1.254 + /** 1.255 + * Iterates all cookies in the cookies service and puts them into the store 1.256 + * if they're session cookies. 1.257 + */ 1.258 + _reloadCookies: function () { 1.259 + let iter = Services.cookies.enumerator; 1.260 + while (iter.hasMoreElements()) { 1.261 + this._updateCookie(iter.getNext()); 1.262 + } 1.263 + } 1.264 +}; 1.265 + 1.266 +/** 1.267 + * The internal cookie storage that keeps track of every active session cookie. 1.268 + * These are stored using maps per host, path, and cookie name. 1.269 + */ 1.270 +let CookieStore = { 1.271 + /** 1.272 + * The internal structure holding all known cookies. 1.273 + * 1.274 + * Host => 1.275 + * Path => 1.276 + * Name => {path: "/", name: "sessionid", secure: true} 1.277 + * 1.278 + * Maps are used for storage but the data structure is equivalent to this: 1.279 + * 1.280 + * this._hosts = { 1.281 + * "www.mozilla.org": { 1.282 + * "/": { 1.283 + * "username": {name: "username", value: "my_name_is", etc...}, 1.284 + * "sessionid": {name: "sessionid", value: "1fdb3a", etc...} 1.285 + * } 1.286 + * }, 1.287 + * "tbpl.mozilla.org": { 1.288 + * "/path": { 1.289 + * "cookiename": {name: "cookiename", value: "value", etc...} 1.290 + * } 1.291 + * } 1.292 + * }; 1.293 + */ 1.294 + _hosts: new Map(), 1.295 + 1.296 + /** 1.297 + * Returns the list of stored session cookies for a given host. 1.298 + * 1.299 + * @param host 1.300 + * A string containing the host name we want to get cookies for. 1.301 + */ 1.302 + getCookiesForHost: function (host) { 1.303 + if (!this._hosts.has(host)) { 1.304 + return []; 1.305 + } 1.306 + 1.307 + let cookies = []; 1.308 + 1.309 + for (let pathToNamesMap of this._hosts.get(host).values()) { 1.310 + cookies.push(...pathToNamesMap.values()); 1.311 + } 1.312 + 1.313 + return cookies; 1.314 + }, 1.315 + 1.316 + /** 1.317 + * Stores a given cookie. 1.318 + * 1.319 + * @param cookie 1.320 + * The nsICookie2 object to add to the storage. 1.321 + */ 1.322 + set: function (cookie) { 1.323 + let jscookie = {host: cookie.host, value: cookie.value}; 1.324 + 1.325 + // Only add properties with non-default values to save a few bytes. 1.326 + if (cookie.path) { 1.327 + jscookie.path = cookie.path; 1.328 + } 1.329 + 1.330 + if (cookie.name) { 1.331 + jscookie.name = cookie.name; 1.332 + } 1.333 + 1.334 + if (cookie.isSecure) { 1.335 + jscookie.secure = true; 1.336 + } 1.337 + 1.338 + if (cookie.isHttpOnly) { 1.339 + jscookie.httponly = true; 1.340 + } 1.341 + 1.342 + if (cookie.expiry < MAX_EXPIRY) { 1.343 + jscookie.expiry = cookie.expiry; 1.344 + } 1.345 + 1.346 + this._ensureMap(cookie).set(cookie.name, jscookie); 1.347 + }, 1.348 + 1.349 + /** 1.350 + * Removes a given cookie. 1.351 + * 1.352 + * @param cookie 1.353 + * The nsICookie2 object to be removed from storage. 1.354 + */ 1.355 + delete: function (cookie) { 1.356 + this._ensureMap(cookie).delete(cookie.name); 1.357 + }, 1.358 + 1.359 + /** 1.360 + * Removes all cookies. 1.361 + */ 1.362 + clear: function () { 1.363 + this._hosts.clear(); 1.364 + }, 1.365 + 1.366 + /** 1.367 + * Creates all maps necessary to store a given cookie. 1.368 + * 1.369 + * @param cookie 1.370 + * The nsICookie2 object to create maps for. 1.371 + * 1.372 + * @return The newly created Map instance mapping cookie names to 1.373 + * internal jscookies, in the given path of the given host. 1.374 + */ 1.375 + _ensureMap: function (cookie) { 1.376 + if (!this._hosts.has(cookie.host)) { 1.377 + this._hosts.set(cookie.host, new Map()); 1.378 + } 1.379 + 1.380 + let pathToNamesMap = this._hosts.get(cookie.host); 1.381 + 1.382 + if (!pathToNamesMap.has(cookie.path)) { 1.383 + pathToNamesMap.set(cookie.path, new Map()); 1.384 + } 1.385 + 1.386 + return pathToNamesMap.get(cookie.path); 1.387 + } 1.388 +};