|
1 # -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
|
2 # This Source Code Form is subject to the terms of the Mozilla Public |
|
3 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
5 |
|
6 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
7 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
8 "resource://gre/modules/PlacesUtils.jsm"); |
|
9 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", |
|
10 "resource://gre/modules/FormHistory.jsm"); |
|
11 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", |
|
12 "resource://gre/modules/Downloads.jsm"); |
|
13 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
14 "resource://gre/modules/Promise.jsm"); |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
16 "resource://gre/modules/Task.jsm"); |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", |
|
18 "resource:///modules/DownloadsCommon.jsm"); |
|
19 |
|
20 function Sanitizer() {} |
|
21 Sanitizer.prototype = { |
|
22 // warning to the caller: this one may raise an exception (e.g. bug #265028) |
|
23 clearItem: function (aItemName) |
|
24 { |
|
25 if (this.items[aItemName].canClear) |
|
26 this.items[aItemName].clear(); |
|
27 }, |
|
28 |
|
29 canClearItem: function (aItemName, aCallback, aArg) |
|
30 { |
|
31 let canClear = this.items[aItemName].canClear; |
|
32 if (typeof canClear == "function") { |
|
33 canClear(aCallback, aArg); |
|
34 return false; |
|
35 } |
|
36 |
|
37 aCallback(aItemName, canClear, aArg); |
|
38 return canClear; |
|
39 }, |
|
40 |
|
41 prefDomain: "", |
|
42 |
|
43 getNameFromPreference: function (aPreferenceName) |
|
44 { |
|
45 return aPreferenceName.substr(this.prefDomain.length); |
|
46 }, |
|
47 |
|
48 /** |
|
49 * Deletes privacy sensitive data in a batch, according to user preferences. |
|
50 * Returns a promise which is resolved if no errors occurred. If an error |
|
51 * occurs, a message is reported to the console and all other items are still |
|
52 * cleared before the promise is finally rejected. |
|
53 */ |
|
54 sanitize: function () |
|
55 { |
|
56 var deferred = Promise.defer(); |
|
57 var psvc = Components.classes["@mozilla.org/preferences-service;1"] |
|
58 .getService(Components.interfaces.nsIPrefService); |
|
59 var branch = psvc.getBranch(this.prefDomain); |
|
60 var seenError = false; |
|
61 |
|
62 // Cache the range of times to clear |
|
63 if (this.ignoreTimespan) |
|
64 var range = null; // If we ignore timespan, clear everything |
|
65 else |
|
66 range = this.range || Sanitizer.getClearRange(); |
|
67 |
|
68 let itemCount = Object.keys(this.items).length; |
|
69 let onItemComplete = function() { |
|
70 if (!--itemCount) { |
|
71 seenError ? deferred.reject() : deferred.resolve(); |
|
72 } |
|
73 }; |
|
74 for (var itemName in this.items) { |
|
75 let item = this.items[itemName]; |
|
76 item.range = range; |
|
77 if ("clear" in item && branch.getBoolPref(itemName)) { |
|
78 let clearCallback = (itemName, aCanClear) => { |
|
79 // Some of these clear() may raise exceptions (see bug #265028) |
|
80 // to sanitize as much as possible, we catch and store them, |
|
81 // rather than fail fast. |
|
82 // Callers should check returned errors and give user feedback |
|
83 // about items that could not be sanitized |
|
84 let item = this.items[itemName]; |
|
85 try { |
|
86 if (aCanClear) |
|
87 item.clear(); |
|
88 } catch(er) { |
|
89 seenError = true; |
|
90 Components.utils.reportError("Error sanitizing " + itemName + |
|
91 ": " + er + "\n"); |
|
92 } |
|
93 onItemComplete(); |
|
94 }; |
|
95 this.canClearItem(itemName, clearCallback); |
|
96 } else { |
|
97 onItemComplete(); |
|
98 } |
|
99 } |
|
100 |
|
101 return deferred.promise; |
|
102 }, |
|
103 |
|
104 // Time span only makes sense in certain cases. Consumers who want |
|
105 // to only clear some private data can opt in by setting this to false, |
|
106 // and can optionally specify a specific range. If timespan is not ignored, |
|
107 // and range is not set, sanitize() will use the value of the timespan |
|
108 // pref to determine a range |
|
109 ignoreTimespan : true, |
|
110 range : null, |
|
111 |
|
112 items: { |
|
113 cache: { |
|
114 clear: function () |
|
115 { |
|
116 var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]. |
|
117 getService(Ci.nsICacheStorageService); |
|
118 try { |
|
119 // Cache doesn't consult timespan, nor does it have the |
|
120 // facility for timespan-based eviction. Wipe it. |
|
121 cache.clear(); |
|
122 } catch(er) {} |
|
123 |
|
124 var imageCache = Cc["@mozilla.org/image/tools;1"]. |
|
125 getService(Ci.imgITools).getImgCacheForDocument(null); |
|
126 try { |
|
127 imageCache.clearCache(false); // true=chrome, false=content |
|
128 } catch(er) {} |
|
129 }, |
|
130 |
|
131 get canClear() |
|
132 { |
|
133 return true; |
|
134 } |
|
135 }, |
|
136 |
|
137 cookies: { |
|
138 clear: function () |
|
139 { |
|
140 var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"] |
|
141 .getService(Ci.nsICookieManager); |
|
142 if (this.range) { |
|
143 // Iterate through the cookies and delete any created after our cutoff. |
|
144 var cookiesEnum = cookieMgr.enumerator; |
|
145 while (cookiesEnum.hasMoreElements()) { |
|
146 var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2); |
|
147 |
|
148 if (cookie.creationTime > this.range[0]) |
|
149 // This cookie was created after our cutoff, clear it |
|
150 cookieMgr.remove(cookie.host, cookie.name, cookie.path, false); |
|
151 } |
|
152 } |
|
153 else { |
|
154 // Remove everything |
|
155 cookieMgr.removeAll(); |
|
156 } |
|
157 |
|
158 // Clear plugin data. |
|
159 const phInterface = Ci.nsIPluginHost; |
|
160 const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL; |
|
161 let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface); |
|
162 |
|
163 // Determine age range in seconds. (-1 means clear all.) We don't know |
|
164 // that this.range[1] is actually now, so we compute age range based |
|
165 // on the lower bound. If this.range results in a negative age, do |
|
166 // nothing. |
|
167 let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000) |
|
168 : -1; |
|
169 if (!this.range || age >= 0) { |
|
170 let tags = ph.getPluginTags(); |
|
171 for (let i = 0; i < tags.length; i++) { |
|
172 try { |
|
173 ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, age); |
|
174 } catch (e) { |
|
175 // If the plugin doesn't support clearing by age, clear everything. |
|
176 if (e.result == Components.results. |
|
177 NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { |
|
178 try { |
|
179 ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, -1); |
|
180 } catch (e) { |
|
181 // Ignore errors from the plugin |
|
182 } |
|
183 } |
|
184 } |
|
185 } |
|
186 } |
|
187 }, |
|
188 |
|
189 get canClear() |
|
190 { |
|
191 return true; |
|
192 } |
|
193 }, |
|
194 |
|
195 offlineApps: { |
|
196 clear: function () |
|
197 { |
|
198 Components.utils.import("resource:///modules/offlineAppCache.jsm"); |
|
199 OfflineAppCacheHelper.clear(); |
|
200 }, |
|
201 |
|
202 get canClear() |
|
203 { |
|
204 return true; |
|
205 } |
|
206 }, |
|
207 |
|
208 history: { |
|
209 clear: function () |
|
210 { |
|
211 if (this.range) |
|
212 PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]); |
|
213 else |
|
214 PlacesUtils.history.removeAllPages(); |
|
215 |
|
216 try { |
|
217 var os = Components.classes["@mozilla.org/observer-service;1"] |
|
218 .getService(Components.interfaces.nsIObserverService); |
|
219 os.notifyObservers(null, "browser:purge-session-history", ""); |
|
220 } |
|
221 catch (e) { } |
|
222 |
|
223 try { |
|
224 var seer = Components.classes["@mozilla.org/network/seer;1"] |
|
225 .getService(Components.interfaces.nsINetworkSeer); |
|
226 seer.reset(); |
|
227 } catch (e) { } |
|
228 }, |
|
229 |
|
230 get canClear() |
|
231 { |
|
232 // bug 347231: Always allow clearing history due to dependencies on |
|
233 // the browser:purge-session-history notification. (like error console) |
|
234 return true; |
|
235 } |
|
236 }, |
|
237 |
|
238 formdata: { |
|
239 clear: function () |
|
240 { |
|
241 // Clear undo history of all searchBars |
|
242 var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] |
|
243 .getService(Components.interfaces.nsIWindowMediator); |
|
244 var windows = windowManager.getEnumerator("navigator:browser"); |
|
245 while (windows.hasMoreElements()) { |
|
246 let currentWindow = windows.getNext(); |
|
247 let currentDocument = currentWindow.document; |
|
248 let searchBar = currentDocument.getElementById("searchbar"); |
|
249 if (searchBar) |
|
250 searchBar.textbox.reset(); |
|
251 let tabBrowser = currentWindow.gBrowser; |
|
252 for (let tab of tabBrowser.tabs) { |
|
253 if (tabBrowser.isFindBarInitialized(tab)) |
|
254 tabBrowser.getFindBar(tab).clear(); |
|
255 } |
|
256 // Clear any saved find value |
|
257 tabBrowser._lastFindValue = ""; |
|
258 } |
|
259 |
|
260 let change = { op: "remove" }; |
|
261 if (this.range) { |
|
262 [ change.firstUsedStart, change.firstUsedEnd ] = this.range; |
|
263 } |
|
264 FormHistory.update(change); |
|
265 }, |
|
266 |
|
267 canClear : function(aCallback, aArg) |
|
268 { |
|
269 var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] |
|
270 .getService(Components.interfaces.nsIWindowMediator); |
|
271 var windows = windowManager.getEnumerator("navigator:browser"); |
|
272 while (windows.hasMoreElements()) { |
|
273 let currentWindow = windows.getNext(); |
|
274 let currentDocument = currentWindow.document; |
|
275 let searchBar = currentDocument.getElementById("searchbar"); |
|
276 if (searchBar) { |
|
277 let transactionMgr = searchBar.textbox.editor.transactionManager; |
|
278 if (searchBar.value || |
|
279 transactionMgr.numberOfUndoItems || |
|
280 transactionMgr.numberOfRedoItems) { |
|
281 aCallback("formdata", true, aArg); |
|
282 return false; |
|
283 } |
|
284 } |
|
285 let tabBrowser = currentWindow.gBrowser; |
|
286 let findBarCanClear = Array.some(tabBrowser.tabs, function (aTab) { |
|
287 return tabBrowser.isFindBarInitialized(aTab) && |
|
288 tabBrowser.getFindBar(aTab).canClear; |
|
289 }); |
|
290 if (findBarCanClear) { |
|
291 aCallback("formdata", true, aArg); |
|
292 return false; |
|
293 } |
|
294 } |
|
295 |
|
296 let count = 0; |
|
297 let countDone = { |
|
298 handleResult : function(aResult) count = aResult, |
|
299 handleError : function(aError) Components.utils.reportError(aError), |
|
300 handleCompletion : |
|
301 function(aReason) { aCallback("formdata", aReason == 0 && count > 0, aArg); } |
|
302 }; |
|
303 FormHistory.count({}, countDone); |
|
304 return false; |
|
305 } |
|
306 }, |
|
307 |
|
308 downloads: { |
|
309 clear: function () |
|
310 { |
|
311 Task.spawn(function () { |
|
312 let filterByTime = null; |
|
313 if (this.range) { |
|
314 // Convert microseconds back to milliseconds for date comparisons. |
|
315 let rangeBeginMs = this.range[0] / 1000; |
|
316 let rangeEndMs = this.range[1] / 1000; |
|
317 filterByTime = download => download.startTime >= rangeBeginMs && |
|
318 download.startTime <= rangeEndMs; |
|
319 } |
|
320 |
|
321 // Clear all completed/cancelled downloads |
|
322 let list = yield Downloads.getList(Downloads.ALL); |
|
323 list.removeFinished(filterByTime); |
|
324 }.bind(this)).then(null, Components.utils.reportError); |
|
325 }, |
|
326 |
|
327 canClear : function(aCallback, aArg) |
|
328 { |
|
329 aCallback("downloads", true, aArg); |
|
330 return false; |
|
331 } |
|
332 }, |
|
333 |
|
334 passwords: { |
|
335 clear: function () |
|
336 { |
|
337 var pwmgr = Components.classes["@mozilla.org/login-manager;1"] |
|
338 .getService(Components.interfaces.nsILoginManager); |
|
339 // Passwords are timeless, and don't respect the timeSpan setting |
|
340 pwmgr.removeAllLogins(); |
|
341 }, |
|
342 |
|
343 get canClear() |
|
344 { |
|
345 var pwmgr = Components.classes["@mozilla.org/login-manager;1"] |
|
346 .getService(Components.interfaces.nsILoginManager); |
|
347 var count = pwmgr.countLogins("", "", ""); // count all logins |
|
348 return (count > 0); |
|
349 } |
|
350 }, |
|
351 |
|
352 sessions: { |
|
353 clear: function () |
|
354 { |
|
355 // clear all auth tokens |
|
356 var sdr = Components.classes["@mozilla.org/security/sdr;1"] |
|
357 .getService(Components.interfaces.nsISecretDecoderRing); |
|
358 sdr.logoutAndTeardown(); |
|
359 |
|
360 // clear FTP and plain HTTP auth sessions |
|
361 var os = Components.classes["@mozilla.org/observer-service;1"] |
|
362 .getService(Components.interfaces.nsIObserverService); |
|
363 os.notifyObservers(null, "net:clear-active-logins", null); |
|
364 }, |
|
365 |
|
366 get canClear() |
|
367 { |
|
368 return true; |
|
369 } |
|
370 }, |
|
371 |
|
372 siteSettings: { |
|
373 clear: function () |
|
374 { |
|
375 // Clear site-specific permissions like "Allow this site to open popups" |
|
376 var pm = Components.classes["@mozilla.org/permissionmanager;1"] |
|
377 .getService(Components.interfaces.nsIPermissionManager); |
|
378 pm.removeAll(); |
|
379 |
|
380 // Clear site-specific settings like page-zoom level |
|
381 var cps = Components.classes["@mozilla.org/content-pref/service;1"] |
|
382 .getService(Components.interfaces.nsIContentPrefService2); |
|
383 cps.removeAllDomains(null); |
|
384 |
|
385 // Clear "Never remember passwords for this site", which is not handled by |
|
386 // the permission manager |
|
387 var pwmgr = Components.classes["@mozilla.org/login-manager;1"] |
|
388 .getService(Components.interfaces.nsILoginManager); |
|
389 var hosts = pwmgr.getAllDisabledHosts(); |
|
390 for each (var host in hosts) { |
|
391 pwmgr.setLoginSavingEnabled(host, true); |
|
392 } |
|
393 }, |
|
394 |
|
395 get canClear() |
|
396 { |
|
397 return true; |
|
398 } |
|
399 } |
|
400 } |
|
401 }; |
|
402 |
|
403 |
|
404 |
|
405 // "Static" members |
|
406 Sanitizer.prefDomain = "privacy.sanitize."; |
|
407 Sanitizer.prefShutdown = "sanitizeOnShutdown"; |
|
408 Sanitizer.prefDidShutdown = "didShutdownSanitize"; |
|
409 |
|
410 // Time span constants corresponding to values of the privacy.sanitize.timeSpan |
|
411 // pref. Used to determine how much history to clear, for various items |
|
412 Sanitizer.TIMESPAN_EVERYTHING = 0; |
|
413 Sanitizer.TIMESPAN_HOUR = 1; |
|
414 Sanitizer.TIMESPAN_2HOURS = 2; |
|
415 Sanitizer.TIMESPAN_4HOURS = 3; |
|
416 Sanitizer.TIMESPAN_TODAY = 4; |
|
417 |
|
418 // Return a 2 element array representing the start and end times, |
|
419 // in the uSec-since-epoch format that PRTime likes. If we should |
|
420 // clear everything, return null. Use ts if it is defined; otherwise |
|
421 // use the timeSpan pref. |
|
422 Sanitizer.getClearRange = function (ts) { |
|
423 if (ts === undefined) |
|
424 ts = Sanitizer.prefs.getIntPref("timeSpan"); |
|
425 if (ts === Sanitizer.TIMESPAN_EVERYTHING) |
|
426 return null; |
|
427 |
|
428 // PRTime is microseconds while JS time is milliseconds |
|
429 var endDate = Date.now() * 1000; |
|
430 switch (ts) { |
|
431 case Sanitizer.TIMESPAN_HOUR : |
|
432 var startDate = endDate - 3600000000; // 1*60*60*1000000 |
|
433 break; |
|
434 case Sanitizer.TIMESPAN_2HOURS : |
|
435 startDate = endDate - 7200000000; // 2*60*60*1000000 |
|
436 break; |
|
437 case Sanitizer.TIMESPAN_4HOURS : |
|
438 startDate = endDate - 14400000000; // 4*60*60*1000000 |
|
439 break; |
|
440 case Sanitizer.TIMESPAN_TODAY : |
|
441 var d = new Date(); // Start with today |
|
442 d.setHours(0); // zero us back to midnight... |
|
443 d.setMinutes(0); |
|
444 d.setSeconds(0); |
|
445 startDate = d.valueOf() * 1000; // convert to epoch usec |
|
446 break; |
|
447 default: |
|
448 throw "Invalid time span for clear private data: " + ts; |
|
449 } |
|
450 return [startDate, endDate]; |
|
451 }; |
|
452 |
|
453 Sanitizer._prefs = null; |
|
454 Sanitizer.__defineGetter__("prefs", function() |
|
455 { |
|
456 return Sanitizer._prefs ? Sanitizer._prefs |
|
457 : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"] |
|
458 .getService(Components.interfaces.nsIPrefService) |
|
459 .getBranch(Sanitizer.prefDomain); |
|
460 }); |
|
461 |
|
462 // Shows sanitization UI |
|
463 Sanitizer.showUI = function(aParentWindow) |
|
464 { |
|
465 var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] |
|
466 .getService(Components.interfaces.nsIWindowWatcher); |
|
467 #ifdef XP_MACOSX |
|
468 ww.openWindow(null, // make this an app-modal window on Mac |
|
469 #else |
|
470 ww.openWindow(aParentWindow, |
|
471 #endif |
|
472 "chrome://browser/content/sanitize.xul", |
|
473 "Sanitize", |
|
474 "chrome,titlebar,dialog,centerscreen,modal", |
|
475 null); |
|
476 }; |
|
477 |
|
478 /** |
|
479 * Deletes privacy sensitive data in a batch, optionally showing the |
|
480 * sanitize UI, according to user preferences |
|
481 */ |
|
482 Sanitizer.sanitize = function(aParentWindow) |
|
483 { |
|
484 Sanitizer.showUI(aParentWindow); |
|
485 }; |
|
486 |
|
487 Sanitizer.onStartup = function() |
|
488 { |
|
489 // we check for unclean exit with pending sanitization |
|
490 Sanitizer._checkAndSanitize(); |
|
491 }; |
|
492 |
|
493 Sanitizer.onShutdown = function() |
|
494 { |
|
495 // we check if sanitization is needed and perform it |
|
496 Sanitizer._checkAndSanitize(); |
|
497 }; |
|
498 |
|
499 // this is called on startup and shutdown, to perform pending sanitizations |
|
500 Sanitizer._checkAndSanitize = function() |
|
501 { |
|
502 const prefs = Sanitizer.prefs; |
|
503 if (prefs.getBoolPref(Sanitizer.prefShutdown) && |
|
504 !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) { |
|
505 // this is a shutdown or a startup after an unclean exit |
|
506 var s = new Sanitizer(); |
|
507 s.prefDomain = "privacy.clearOnShutdown."; |
|
508 s.sanitize().then(function() { |
|
509 prefs.setBoolPref(Sanitizer.prefDidShutdown, true); |
|
510 }); |
|
511 } |
|
512 }; |