|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ["SessionCookies"]; |
|
8 |
|
9 const Cu = Components.utils; |
|
10 const Ci = Components.interfaces; |
|
11 |
|
12 Cu.import("resource://gre/modules/Services.jsm", this); |
|
13 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
14 |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "Utils", |
|
16 "resource:///modules/sessionstore/Utils.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel", |
|
18 "resource:///modules/sessionstore/PrivacyLevel.jsm"); |
|
19 |
|
20 // MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision. |
|
21 const MAX_EXPIRY = Math.pow(2, 62); |
|
22 |
|
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 }, |
|
30 |
|
31 getHostsForWindow: function (window, checkPrivacy = false) { |
|
32 return SessionCookiesInternal.getHostsForWindow(window, checkPrivacy); |
|
33 } |
|
34 }); |
|
35 |
|
36 /** |
|
37 * The internal API. |
|
38 */ |
|
39 let SessionCookiesInternal = { |
|
40 /** |
|
41 * Stores whether we're initialized, yet. |
|
42 */ |
|
43 _initialized: false, |
|
44 |
|
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(); |
|
56 |
|
57 for (let window of windows) { |
|
58 let cookies = []; |
|
59 |
|
60 // Collect all hosts for the current window. |
|
61 let hosts = this.getHostsForWindow(window, true); |
|
62 |
|
63 for (let host of Object.keys(hosts)) { |
|
64 let isPinned = hosts[host]; |
|
65 |
|
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 } |
|
75 |
|
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 }, |
|
84 |
|
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 = {}; |
|
100 |
|
101 for (let tab of window.tabs) { |
|
102 for (let entry of tab.entries) { |
|
103 this._extractHostsFromEntry(entry, hosts, checkPrivacy, tab.pinned); |
|
104 } |
|
105 } |
|
106 |
|
107 return hosts; |
|
108 }, |
|
109 |
|
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 }, |
|
137 |
|
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 }, |
|
149 |
|
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; |
|
167 |
|
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 } |
|
181 |
|
182 if (entry.children) { |
|
183 for (let child of entry.children) { |
|
184 this._extractHostsFromEntry(child, hosts, checkPrivacy, isPinned); |
|
185 } |
|
186 } |
|
187 }, |
|
188 |
|
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 }, |
|
219 |
|
220 /** |
|
221 * Updates or adds a given cookie to the store. |
|
222 */ |
|
223 _updateCookie: function (cookie) { |
|
224 cookie.QueryInterface(Ci.nsICookie2); |
|
225 |
|
226 if (cookie.isSession) { |
|
227 CookieStore.set(cookie); |
|
228 } |
|
229 }, |
|
230 |
|
231 /** |
|
232 * Removes a given cookie from the store. |
|
233 */ |
|
234 _removeCookie: function (cookie) { |
|
235 cookie.QueryInterface(Ci.nsICookie2); |
|
236 |
|
237 if (cookie.isSession) { |
|
238 CookieStore.delete(cookie); |
|
239 } |
|
240 }, |
|
241 |
|
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 }, |
|
250 |
|
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 }; |
|
262 |
|
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(), |
|
292 |
|
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 } |
|
303 |
|
304 let cookies = []; |
|
305 |
|
306 for (let pathToNamesMap of this._hosts.get(host).values()) { |
|
307 cookies.push(...pathToNamesMap.values()); |
|
308 } |
|
309 |
|
310 return cookies; |
|
311 }, |
|
312 |
|
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}; |
|
321 |
|
322 // Only add properties with non-default values to save a few bytes. |
|
323 if (cookie.path) { |
|
324 jscookie.path = cookie.path; |
|
325 } |
|
326 |
|
327 if (cookie.name) { |
|
328 jscookie.name = cookie.name; |
|
329 } |
|
330 |
|
331 if (cookie.isSecure) { |
|
332 jscookie.secure = true; |
|
333 } |
|
334 |
|
335 if (cookie.isHttpOnly) { |
|
336 jscookie.httponly = true; |
|
337 } |
|
338 |
|
339 if (cookie.expiry < MAX_EXPIRY) { |
|
340 jscookie.expiry = cookie.expiry; |
|
341 } |
|
342 |
|
343 this._ensureMap(cookie).set(cookie.name, jscookie); |
|
344 }, |
|
345 |
|
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 }, |
|
355 |
|
356 /** |
|
357 * Removes all cookies. |
|
358 */ |
|
359 clear: function () { |
|
360 this._hosts.clear(); |
|
361 }, |
|
362 |
|
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 } |
|
376 |
|
377 let pathToNamesMap = this._hosts.get(cookie.host); |
|
378 |
|
379 if (!pathToNamesMap.has(cookie.path)) { |
|
380 pathToNamesMap.set(cookie.path, new Map()); |
|
381 } |
|
382 |
|
383 return pathToNamesMap.get(cookie.path); |
|
384 } |
|
385 }; |