Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["SessionCookies"];
9 const Cu = Components.utils;
10 const Ci = Components.interfaces;
12 Cu.import("resource://gre/modules/Services.jsm", this);
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
15 XPCOMUtils.defineLazyModuleGetter(this, "Utils",
16 "resource:///modules/sessionstore/Utils.jsm");
17 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
18 "resource:///modules/sessionstore/PrivacyLevel.jsm");
20 // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
21 const MAX_EXPIRY = Math.pow(2, 62);
23 /**
24 * The external API implemented by the SessionCookies module.
25 */
26 this.SessionCookies = Object.freeze({
27 update: function (windows) {
28 SessionCookiesInternal.update(windows);
29 },
31 getHostsForWindow: function (window, checkPrivacy = false) {
32 return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy);
33 }
34 });
36 /**
37 * The internal API.
38 */
39 let SessionCookiesInternal = {
40 /**
41 * Stores whether we're initialized, yet.
42 */
43 _initialized: false,
45 /**
46 * Retrieve the list of all hosts contained in the given windows' session
47 * history entries (per window) and collect the associated cookies for those
48 * hosts, if any. The given state object is being modified.
49 *
50 * @param windows
51 * Array of window state objects.
52 * [{ tabs: [...], cookies: [...] }, ...]
53 */
54 update: function (windows) {
55 this._ensureInitialized();
57 for (let window of windows) {
58 let cookies = [];
60 // Collect all hosts for the current window.
61 let hosts = this.getHostsForWindow(window, true);
63 for (let host of Object.keys(hosts)) {
64 let isPinned = hosts[host];
66 for (let cookie of CookieStore.getCookiesForHost(host)) {
67 // _getCookiesForHost() will only return hosts with the right privacy
68 // rules, so there is no need to do anything special with this call
69 // to PrivacyLevel.canSave().
70 if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
71 cookies.push(cookie);
72 }
73 }
74 }
76 // Don't include/keep empty cookie sections.
77 if (cookies.length) {
78 window.cookies = cookies;
79 } else if ("cookies" in window) {
80 delete window.cookies;
81 }
82 }
83 },
85 /**
86 * Returns a map of all hosts for a given window that we might want to
87 * collect cookies for.
88 *
89 * @param window
90 * A window state object containing tabs with history entries.
91 * @param checkPrivacy (bool)
92 * Whether to check the privacy level for each host.
93 * @return {object} A map of hosts for a given window state object. The keys
94 * will be hosts, the values are boolean and determine
95 * whether we will use the deferred privacy level when
96 * checking how much data to save on quitting.
97 */
98 getHostsForWindow: function (window, checkPrivacy = false) {
99 let hosts = {};
101 for (let tab of window.tabs) {
102 for (let entry of tab.entries) {
103 this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned);
104 }
105 }
107 return hosts;
108 },
110 /**
111 * Handles observers notifications that are sent whenever cookies are added,
112 * changed, or removed. Ensures that the storage is updated accordingly.
113 */
114 observe: function (subject, topic, data) {
115 switch (data) {
116 case "added":
117 case "changed":
118 this._updateCookie(subject);
119 break;
120 case "deleted":
121 this._removeCookie(subject);
122 break;
123 case "cleared":
124 CookieStore.clear();
125 break;
126 case "batch-deleted":
127 this._removeCookies(subject);
128 break;
129 case "reload":
130 CookieStore.clear();
131 this._reloadCookies();
132 break;
133 default:
134 throw new Error("Unhandled cookie-changed notification.");
135 }
136 },
138 /**
139 * If called for the first time in a session, iterates all cookies in the
140 * cookies service and puts them into the store if they're session cookies.
141 */
142 _ensureInitialized: function () {
143 if (!this._initialized) {
144 this._reloadCookies();
145 this._initialized = true;
146 Services.obs.addObserver(this, "cookie-changed", false);
147 }
148 },
150 /**
151 * Fill a given map with hosts found in the given entry's session history and
152 * any child entries.
153 *
154 * @param entry
155 * the history entry, serialized
156 * @param hosts
157 * the hash that will be used to store hosts eg, { hostname: true }
158 * @param checkPrivacy
159 * should we check the privacy level for https
160 * @param isPinned
161 * is the entry we're evaluating for a pinned tab; used only if
162 * checkPrivacy
163 */
164 _extractHostsFromEntry: function (entry, hosts, checkPrivacy, isPinned) {
165 let host = entry._host;
166 let scheme = entry._scheme;
168 // If host & scheme aren't defined, then we are likely here in the startup
169 // process via _splitCookiesFromWindow. In that case, we'll turn entry.url
170 // into an nsIURI and get host/scheme from that. This will throw for about:
171 // urls in which case we don't need to do anything.
172 if (!host && !scheme) {
173 try {
174 let uri = Utils.makeURI(entry.url);
175 host = uri.host;
176 scheme = uri.scheme;
177 this._extractHostsFromHostScheme(host, scheme, hosts, checkPrivacy, isPinned);
178 }
179 catch (ex) { }
180 }
182 if (entry.children) {
183 for (let child of entry.children) {
184 this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned);
185 }
186 }
187 },
189 /**
190 * Add a given host to a given map of hosts if the privacy level allows
191 * saving cookie data for it.
192 *
193 * @param host
194 * the host of a uri (usually via nsIURI.host)
195 * @param scheme
196 * the scheme of a uri (usually via nsIURI.scheme)
197 * @param hosts
198 * the hash that will be used to store hosts eg, { hostname: true }
199 * @param checkPrivacy
200 * should we check the privacy level for https
201 * @param isPinned
202 * is the entry we're evaluating for a pinned tab; used only if
203 * checkPrivacy
204 */
205 _extractHostsFromHostScheme:
206 function (host, scheme, hosts, checkPrivacy, isPinned) {
207 // host and scheme may not be set (for about: urls for example), in which
208 // case testing scheme will be sufficient.
209 if (/https?/.test(scheme) && !hosts[host] &&
210 (!checkPrivacy ||
211 PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
212 // By setting this to true or false, we can determine when looking at
213 // the host in update() if we should check for privacy.
214 hosts[host] = isPinned;
215 } else if (scheme == "file") {
216 hosts[host] = true;
217 }
218 },
220 /**
221 * Updates or adds a given cookie to the store.
222 */
223 _updateCookie: function (cookie) {
224 cookie.QueryInterface(Ci.nsICookie2);
226 if (cookie.isSession) {
227 CookieStore.set(cookie);
228 }
229 },
231 /**
232 * Removes a given cookie from the store.
233 */
234 _removeCookie: function (cookie) {
235 cookie.QueryInterface(Ci.nsICookie2);
237 if (cookie.isSession) {
238 CookieStore.delete(cookie);
239 }
240 },
242 /**
243 * Removes a given list of cookies from the store.
244 */
245 _removeCookies: function (cookies) {
246 for (let i = 0; i < cookies.length; i++) {
247 this._removeCookie(cookies.queryElementAt(i, Ci.nsICookie2));
248 }
249 },
251 /**
252 * Iterates all cookies in the cookies service and puts them into the store
253 * if they're session cookies.
254 */
255 _reloadCookies: function () {
256 let iter = Services.cookies.enumerator;
257 while (iter.hasMoreElements()) {
258 this._updateCookie(iter.getNext());
259 }
260 }
261 };
263 /**
264 * The internal cookie storage that keeps track of every active session cookie.
265 * These are stored using maps per host, path, and cookie name.
266 */
267 let CookieStore = {
268 /**
269 * The internal structure holding all known cookies.
270 *
271 * Host =>
272 * Path =>
273 * Name => {path: "/", name: "sessionid", secure: true}
274 *
275 * Maps are used for storage but the data structure is equivalent to this:
276 *
277 * this._hosts = {
278 * "www.mozilla.org": {
279 * "/": {
280 * "username": {name: "username", value: "my_name_is", etc...},
281 * "sessionid": {name: "sessionid", value: "1fdb3a", etc...}
282 * }
283 * },
284 * "tbpl.mozilla.org": {
285 * "/path": {
286 * "cookiename": {name: "cookiename", value: "value", etc...}
287 * }
288 * }
289 * };
290 */
291 _hosts: new Map(),
293 /**
294 * Returns the list of stored session cookies for a given host.
295 *
296 * @param host
297 * A string containing the host name we want to get cookies for.
298 */
299 getCookiesForHost: function (host) {
300 if (!this._hosts.has(host)) {
301 return [];
302 }
304 let cookies = [];
306 for (let pathToNamesMap of this._hosts.get(host).values()) {
307 cookies.push(...pathToNamesMap.values());
308 }
310 return cookies;
311 },
313 /**
314 * Stores a given cookie.
315 *
316 * @param cookie
317 * The nsICookie2 object to add to the storage.
318 */
319 set: function (cookie) {
320 let jscookie = {host: cookie.host, value: cookie.value};
322 // Only add properties with non-default values to save a few bytes.
323 if (cookie.path) {
324 jscookie.path = cookie.path;
325 }
327 if (cookie.name) {
328 jscookie.name = cookie.name;
329 }
331 if (cookie.isSecure) {
332 jscookie.secure = true;
333 }
335 if (cookie.isHttpOnly) {
336 jscookie.httponly = true;
337 }
339 if (cookie.expiry < MAX_EXPIRY) {
340 jscookie.expiry = cookie.expiry;
341 }
343 this._ensureMap(cookie).set(cookie.name, jscookie);
344 },
346 /**
347 * Removes a given cookie.
348 *
349 * @param cookie
350 * The nsICookie2 object to be removed from storage.
351 */
352 delete: function (cookie) {
353 this._ensureMap(cookie).delete(cookie.name);
354 },
356 /**
357 * Removes all cookies.
358 */
359 clear: function () {
360 this._hosts.clear();
361 },
363 /**
364 * Creates all maps necessary to store a given cookie.
365 *
366 * @param cookie
367 * The nsICookie2 object to create maps for.
368 *
369 * @return The newly created Map instance mapping cookie names to
370 * internal jscookies, in the given path of the given host.
371 */
372 _ensureMap: function (cookie) {
373 if (!this._hosts.has(cookie.host)) {
374 this._hosts.set(cookie.host, new Map());
375 }
377 let pathToNamesMap = this._hosts.get(cookie.host);
379 if (!pathToNamesMap.has(cookie.path)) {
380 pathToNamesMap.set(cookie.path, new Map());
381 }
383 return pathToNamesMap.get(cookie.path);
384 }
385 };