|
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 |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 let Ci = Components.interfaces; |
|
6 let Cc = Components.classes; |
|
7 let Cu = Components.utils; |
|
8 |
|
9 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
10 Cu.import("resource://gre/modules/Services.jsm"); |
|
11 Cu.import("resource://gre/modules/DownloadUtils.jsm"); |
|
12 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
13 Cu.import("resource://gre/modules/ForgetAboutSite.jsm"); |
|
14 |
|
15 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
16 "resource://gre/modules/PluralForm.jsm"); |
|
17 |
|
18 let gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"]. |
|
19 getService(Ci.nsIFaviconService); |
|
20 |
|
21 let gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"]. |
|
22 getService(Ci.nsPIPlacesDatabase). |
|
23 DBConnection. |
|
24 clone(true); |
|
25 |
|
26 let gSitesStmt = gPlacesDatabase.createAsyncStatement( |
|
27 "SELECT get_unreversed_host(rev_host) AS host " + |
|
28 "FROM moz_places " + |
|
29 "WHERE rev_host > '.' " + |
|
30 "AND visit_count > 0 " + |
|
31 "GROUP BY rev_host " + |
|
32 "ORDER BY MAX(frecency) DESC " + |
|
33 "LIMIT :limit"); |
|
34 |
|
35 let gVisitStmt = gPlacesDatabase.createAsyncStatement( |
|
36 "SELECT SUM(visit_count) AS count " + |
|
37 "FROM moz_places " + |
|
38 "WHERE rev_host = :rev_host"); |
|
39 |
|
40 /** |
|
41 * Permission types that should be tested with testExactPermission, as opposed |
|
42 * to testPermission. This is based on what consumers use to test these permissions. |
|
43 */ |
|
44 let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"]; |
|
45 |
|
46 /** |
|
47 * Site object represents a single site, uniquely identified by a host. |
|
48 */ |
|
49 function Site(host) { |
|
50 this.host = host; |
|
51 this.listitem = null; |
|
52 |
|
53 this.httpURI = NetUtil.newURI("http://" + this.host); |
|
54 this.httpsURI = NetUtil.newURI("https://" + this.host); |
|
55 } |
|
56 |
|
57 Site.prototype = { |
|
58 /** |
|
59 * Gets the favicon to use for the site. The callback only gets called if |
|
60 * a favicon is found for either the http URI or the https URI. |
|
61 * |
|
62 * @param aCallback |
|
63 * A callback function that takes a favicon image URL as a parameter. |
|
64 */ |
|
65 getFavicon: function Site_getFavicon(aCallback) { |
|
66 function invokeCallback(aFaviconURI) { |
|
67 try { |
|
68 // Use getFaviconLinkForIcon to get image data from the database instead |
|
69 // of using the favicon URI to fetch image data over the network. |
|
70 aCallback(gFaviconService.getFaviconLinkForIcon(aFaviconURI).spec); |
|
71 } catch (e) { |
|
72 Cu.reportError("AboutPermissions: " + e); |
|
73 } |
|
74 } |
|
75 |
|
76 // Try to find favicon for both URIs, but always prefer the https favicon. |
|
77 gFaviconService.getFaviconURLForPage(this.httpsURI, function (aURI) { |
|
78 if (aURI) { |
|
79 invokeCallback(aURI); |
|
80 } else { |
|
81 gFaviconService.getFaviconURLForPage(this.httpURI, function (aURI) { |
|
82 if (aURI) { |
|
83 invokeCallback(aURI); |
|
84 } |
|
85 }); |
|
86 } |
|
87 }.bind(this)); |
|
88 }, |
|
89 |
|
90 /** |
|
91 * Gets the number of history visits for the site. |
|
92 * |
|
93 * @param aCallback |
|
94 * A function that takes the visit count (a number) as a parameter. |
|
95 */ |
|
96 getVisitCount: function Site_getVisitCount(aCallback) { |
|
97 let rev_host = this.host.split("").reverse().join("") + "."; |
|
98 gVisitStmt.params.rev_host = rev_host; |
|
99 gVisitStmt.executeAsync({ |
|
100 handleResult: function(aResults) { |
|
101 let row = aResults.getNextRow(); |
|
102 let count = row.getResultByName("count") || 0; |
|
103 try { |
|
104 aCallback(count); |
|
105 } catch (e) { |
|
106 Cu.reportError("AboutPermissions: " + e); |
|
107 } |
|
108 }, |
|
109 handleError: function(aError) { |
|
110 Cu.reportError("AboutPermissions: " + aError); |
|
111 }, |
|
112 handleCompletion: function(aReason) { |
|
113 } |
|
114 }); |
|
115 }, |
|
116 |
|
117 /** |
|
118 * Gets the permission value stored for a specified permission type. |
|
119 * |
|
120 * @param aType |
|
121 * The permission type string stored in permission manager. |
|
122 * e.g. "cookie", "geo", "indexedDB", "popup", "image" |
|
123 * @param aResultObj |
|
124 * An object that stores the permission value set for aType. |
|
125 * |
|
126 * @return A boolean indicating whether or not a permission is set. |
|
127 */ |
|
128 getPermission: function Site_getPermission(aType, aResultObj) { |
|
129 // Password saving isn't a nsIPermissionManager permission type, so handle |
|
130 // it seperately. |
|
131 if (aType == "password") { |
|
132 aResultObj.value = this.loginSavingEnabled ? |
|
133 Ci.nsIPermissionManager.ALLOW_ACTION : |
|
134 Ci.nsIPermissionManager.DENY_ACTION; |
|
135 return true; |
|
136 } |
|
137 |
|
138 let permissionValue; |
|
139 if (TEST_EXACT_PERM_TYPES.indexOf(aType) == -1) { |
|
140 permissionValue = Services.perms.testPermission(this.httpURI, aType); |
|
141 } else { |
|
142 permissionValue = Services.perms.testExactPermission(this.httpURI, aType); |
|
143 } |
|
144 aResultObj.value = permissionValue; |
|
145 |
|
146 return permissionValue != Ci.nsIPermissionManager.UNKNOWN_ACTION; |
|
147 }, |
|
148 |
|
149 /** |
|
150 * Sets a permission for the site given a permission type and value. |
|
151 * |
|
152 * @param aType |
|
153 * The permission type string stored in permission manager. |
|
154 * e.g. "cookie", "geo", "indexedDB", "popup", "image" |
|
155 * @param aPerm |
|
156 * The permission value to set for the permission type. This should |
|
157 * be one of the constants defined in nsIPermissionManager. |
|
158 */ |
|
159 setPermission: function Site_setPermission(aType, aPerm) { |
|
160 // Password saving isn't a nsIPermissionManager permission type, so handle |
|
161 // it seperately. |
|
162 if (aType == "password") { |
|
163 this.loginSavingEnabled = aPerm == Ci.nsIPermissionManager.ALLOW_ACTION; |
|
164 return; |
|
165 } |
|
166 |
|
167 // Using httpURI is kind of bogus, but the permission manager stores the |
|
168 // permission for the host, so the right thing happens in the end. |
|
169 Services.perms.add(this.httpURI, aType, aPerm); |
|
170 }, |
|
171 |
|
172 /** |
|
173 * Clears a user-set permission value for the site given a permission type. |
|
174 * |
|
175 * @param aType |
|
176 * The permission type string stored in permission manager. |
|
177 * e.g. "cookie", "geo", "indexedDB", "popup", "image" |
|
178 */ |
|
179 clearPermission: function Site_clearPermission(aType) { |
|
180 Services.perms.remove(this.host, aType); |
|
181 }, |
|
182 |
|
183 /** |
|
184 * Gets cookies stored for the site. This does not return cookies stored |
|
185 * for the base domain, only the exact hostname stored for the site. |
|
186 * |
|
187 * @return An array of the cookies set for the site. |
|
188 */ |
|
189 get cookies() { |
|
190 let cookies = []; |
|
191 let enumerator = Services.cookies.getCookiesFromHost(this.host); |
|
192 while (enumerator.hasMoreElements()) { |
|
193 let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); |
|
194 // getCookiesFromHost returns cookies for base domain, but we only want |
|
195 // the cookies for the exact domain. |
|
196 if (cookie.rawHost == this.host) { |
|
197 cookies.push(cookie); |
|
198 } |
|
199 } |
|
200 return cookies; |
|
201 }, |
|
202 |
|
203 /** |
|
204 * Removes a set of specific cookies from the browser. |
|
205 */ |
|
206 clearCookies: function Site_clearCookies() { |
|
207 this.cookies.forEach(function(aCookie) { |
|
208 Services.cookies.remove(aCookie.host, aCookie.name, aCookie.path, false); |
|
209 }); |
|
210 }, |
|
211 |
|
212 /** |
|
213 * Gets logins stored for the site. |
|
214 * |
|
215 * @return An array of the logins stored for the site. |
|
216 */ |
|
217 get logins() { |
|
218 let httpLogins = Services.logins.findLogins({}, this.httpURI.prePath, "", ""); |
|
219 let httpsLogins = Services.logins.findLogins({}, this.httpsURI.prePath, "", ""); |
|
220 return httpLogins.concat(httpsLogins); |
|
221 }, |
|
222 |
|
223 get loginSavingEnabled() { |
|
224 // Only say that login saving is blocked if it is blocked for both http and https. |
|
225 return Services.logins.getLoginSavingEnabled(this.httpURI.prePath) && |
|
226 Services.logins.getLoginSavingEnabled(this.httpsURI.prePath); |
|
227 }, |
|
228 |
|
229 set loginSavingEnabled(isEnabled) { |
|
230 Services.logins.setLoginSavingEnabled(this.httpURI.prePath, isEnabled); |
|
231 Services.logins.setLoginSavingEnabled(this.httpsURI.prePath, isEnabled); |
|
232 }, |
|
233 |
|
234 /** |
|
235 * Removes all data from the browser corresponding to the site. |
|
236 */ |
|
237 forgetSite: function Site_forgetSite() { |
|
238 ForgetAboutSite.removeDataFromDomain(this.host); |
|
239 } |
|
240 } |
|
241 |
|
242 /** |
|
243 * PermissionDefaults object keeps track of default permissions for sites based |
|
244 * on global preferences. |
|
245 * |
|
246 * Inspired by pageinfo/permissions.js |
|
247 */ |
|
248 let PermissionDefaults = { |
|
249 UNKNOWN: Ci.nsIPermissionManager.UNKNOWN_ACTION, // 0 |
|
250 ALLOW: Ci.nsIPermissionManager.ALLOW_ACTION, // 1 |
|
251 DENY: Ci.nsIPermissionManager.DENY_ACTION, // 2 |
|
252 SESSION: Ci.nsICookiePermission.ACCESS_SESSION, // 8 |
|
253 |
|
254 get password() { |
|
255 if (Services.prefs.getBoolPref("signon.rememberSignons")) { |
|
256 return this.ALLOW; |
|
257 } |
|
258 return this.DENY; |
|
259 }, |
|
260 set password(aValue) { |
|
261 let value = (aValue != this.DENY); |
|
262 Services.prefs.setBoolPref("signon.rememberSignons", value); |
|
263 }, |
|
264 |
|
265 // For use with network.cookie.* prefs. |
|
266 COOKIE_ACCEPT: 0, |
|
267 COOKIE_DENY: 2, |
|
268 COOKIE_NORMAL: 0, |
|
269 COOKIE_SESSION: 2, |
|
270 |
|
271 get cookie() { |
|
272 if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == this.COOKIE_DENY) { |
|
273 return this.DENY; |
|
274 } |
|
275 |
|
276 if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == this.COOKIE_SESSION) { |
|
277 return this.SESSION; |
|
278 } |
|
279 return this.ALLOW; |
|
280 }, |
|
281 set cookie(aValue) { |
|
282 let value = (aValue == this.DENY) ? this.COOKIE_DENY : this.COOKIE_ACCEPT; |
|
283 Services.prefs.setIntPref("network.cookie.cookieBehavior", value); |
|
284 |
|
285 let lifetimeValue = aValue == this.SESSION ? this.COOKIE_SESSION : |
|
286 this.COOKIE_NORMAL; |
|
287 Services.prefs.setIntPref("network.cookie.lifetimePolicy", lifetimeValue); |
|
288 }, |
|
289 |
|
290 get geo() { |
|
291 if (!Services.prefs.getBoolPref("geo.enabled")) { |
|
292 return this.DENY; |
|
293 } |
|
294 // We always ask for permission to share location with a specific site, so |
|
295 // there is no global ALLOW. |
|
296 return this.UNKNOWN; |
|
297 }, |
|
298 set geo(aValue) { |
|
299 let value = (aValue != this.DENY); |
|
300 Services.prefs.setBoolPref("geo.enabled", value); |
|
301 }, |
|
302 |
|
303 get indexedDB() { |
|
304 if (!Services.prefs.getBoolPref("dom.indexedDB.enabled")) { |
|
305 return this.DENY; |
|
306 } |
|
307 // We always ask for permission to enable indexedDB storage for a specific |
|
308 // site, so there is no global ALLOW. |
|
309 return this.UNKNOWN; |
|
310 }, |
|
311 set indexedDB(aValue) { |
|
312 let value = (aValue != this.DENY); |
|
313 Services.prefs.setBoolPref("dom.indexedDB.enabled", value); |
|
314 }, |
|
315 |
|
316 get popup() { |
|
317 if (Services.prefs.getBoolPref("dom.disable_open_during_load")) { |
|
318 return this.DENY; |
|
319 } |
|
320 return this.ALLOW; |
|
321 }, |
|
322 set popup(aValue) { |
|
323 let value = (aValue == this.DENY); |
|
324 Services.prefs.setBoolPref("dom.disable_open_during_load", value); |
|
325 }, |
|
326 |
|
327 get fullscreen() { |
|
328 if (!Services.prefs.getBoolPref("full-screen-api.enabled")) { |
|
329 return this.DENY; |
|
330 } |
|
331 return this.UNKNOWN; |
|
332 }, |
|
333 set fullscreen(aValue) { |
|
334 let value = (aValue != this.DENY); |
|
335 Services.prefs.setBoolPref("full-screen-api.enabled", value); |
|
336 }, |
|
337 |
|
338 get camera() this.UNKNOWN, |
|
339 get microphone() this.UNKNOWN |
|
340 }; |
|
341 |
|
342 /** |
|
343 * AboutPermissions manages the about:permissions page. |
|
344 */ |
|
345 let AboutPermissions = { |
|
346 /** |
|
347 * Number of sites to return from the places database. |
|
348 */ |
|
349 PLACES_SITES_LIMIT: 50, |
|
350 |
|
351 /** |
|
352 * When adding sites to the dom sites-list, divide workload into intervals. |
|
353 */ |
|
354 LIST_BUILD_CHUNK: 5, // interval size |
|
355 LIST_BUILD_DELAY: 100, // delay between intervals |
|
356 |
|
357 /** |
|
358 * Stores a mapping of host strings to Site objects. |
|
359 */ |
|
360 _sites: {}, |
|
361 |
|
362 sitesList: null, |
|
363 _selectedSite: null, |
|
364 |
|
365 /** |
|
366 * For testing, track initializations so we can send notifications |
|
367 */ |
|
368 _initPlacesDone: false, |
|
369 _initServicesDone: false, |
|
370 |
|
371 /** |
|
372 * This reflects the permissions that we expose in the UI. These correspond |
|
373 * to permission type strings in the permission manager, PermissionDefaults, |
|
374 * and element ids in aboutPermissions.xul. |
|
375 * |
|
376 * Potential future additions: "sts/use", "sts/subd" |
|
377 */ |
|
378 _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", |
|
379 "fullscreen", "camera", "microphone"], |
|
380 |
|
381 /** |
|
382 * Permissions that don't have a global "Allow" option. |
|
383 */ |
|
384 _noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"], |
|
385 |
|
386 /** |
|
387 * Permissions that don't have a global "Deny" option. |
|
388 */ |
|
389 _noGlobalDeny: ["camera", "microphone"], |
|
390 |
|
391 _stringBundle: Services.strings. |
|
392 createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"), |
|
393 |
|
394 /** |
|
395 * Called on page load. |
|
396 */ |
|
397 init: function() { |
|
398 this.sitesList = document.getElementById("sites-list"); |
|
399 |
|
400 this.getSitesFromPlaces(); |
|
401 |
|
402 this.enumerateServicesGenerator = this.getEnumerateServicesGenerator(); |
|
403 setTimeout(this.enumerateServicesDriver.bind(this), this.LIST_BUILD_DELAY); |
|
404 |
|
405 // Attach observers in case data changes while the page is open. |
|
406 Services.prefs.addObserver("signon.rememberSignons", this, false); |
|
407 Services.prefs.addObserver("network.cookie.", this, false); |
|
408 Services.prefs.addObserver("geo.enabled", this, false); |
|
409 Services.prefs.addObserver("dom.indexedDB.enabled", this, false); |
|
410 Services.prefs.addObserver("dom.disable_open_during_load", this, false); |
|
411 Services.prefs.addObserver("full-screen-api.enabled", this, false); |
|
412 |
|
413 Services.obs.addObserver(this, "perm-changed", false); |
|
414 Services.obs.addObserver(this, "passwordmgr-storage-changed", false); |
|
415 Services.obs.addObserver(this, "cookie-changed", false); |
|
416 Services.obs.addObserver(this, "browser:purge-domain-data", false); |
|
417 |
|
418 this._observersInitialized = true; |
|
419 Services.obs.notifyObservers(null, "browser-permissions-preinit", null); |
|
420 }, |
|
421 |
|
422 /** |
|
423 * Called on page unload. |
|
424 */ |
|
425 cleanUp: function() { |
|
426 if (this._observersInitialized) { |
|
427 Services.prefs.removeObserver("signon.rememberSignons", this, false); |
|
428 Services.prefs.removeObserver("network.cookie.", this, false); |
|
429 Services.prefs.removeObserver("geo.enabled", this, false); |
|
430 Services.prefs.removeObserver("dom.indexedDB.enabled", this, false); |
|
431 Services.prefs.removeObserver("dom.disable_open_during_load", this, false); |
|
432 Services.prefs.removeObserver("full-screen-api.enabled", this, false); |
|
433 |
|
434 Services.obs.removeObserver(this, "perm-changed"); |
|
435 Services.obs.removeObserver(this, "passwordmgr-storage-changed"); |
|
436 Services.obs.removeObserver(this, "cookie-changed"); |
|
437 Services.obs.removeObserver(this, "browser:purge-domain-data"); |
|
438 } |
|
439 |
|
440 gSitesStmt.finalize(); |
|
441 gVisitStmt.finalize(); |
|
442 gPlacesDatabase.asyncClose(null); |
|
443 }, |
|
444 |
|
445 observe: function (aSubject, aTopic, aData) { |
|
446 switch(aTopic) { |
|
447 case "perm-changed": |
|
448 // Permissions changes only affect individual sites. |
|
449 if (!this._selectedSite) { |
|
450 break; |
|
451 } |
|
452 // aSubject is null when nsIPermisionManager::removeAll() is called. |
|
453 if (!aSubject) { |
|
454 this._supportedPermissions.forEach(function(aType){ |
|
455 this.updatePermission(aType); |
|
456 }, this); |
|
457 break; |
|
458 } |
|
459 let permission = aSubject.QueryInterface(Ci.nsIPermission); |
|
460 // We can't compare selectedSite.host and permission.host here because |
|
461 // we need to handle the case where a parent domain was changed in a |
|
462 // way that affects the subdomain. |
|
463 if (this._supportedPermissions.indexOf(permission.type) != -1) { |
|
464 this.updatePermission(permission.type); |
|
465 } |
|
466 break; |
|
467 case "nsPref:changed": |
|
468 this._supportedPermissions.forEach(function(aType){ |
|
469 this.updatePermission(aType); |
|
470 }, this); |
|
471 break; |
|
472 case "passwordmgr-storage-changed": |
|
473 this.updatePermission("password"); |
|
474 if (this._selectedSite) { |
|
475 this.updatePasswordsCount(); |
|
476 } |
|
477 break; |
|
478 case "cookie-changed": |
|
479 if (this._selectedSite) { |
|
480 this.updateCookiesCount(); |
|
481 } |
|
482 break; |
|
483 case "browser:purge-domain-data": |
|
484 this.deleteFromSitesList(aData); |
|
485 break; |
|
486 } |
|
487 }, |
|
488 |
|
489 /** |
|
490 * Creates Site objects for the top-frecency sites in the places database and stores |
|
491 * them in _sites. The number of sites created is controlled by PLACES_SITES_LIMIT. |
|
492 */ |
|
493 getSitesFromPlaces: function() { |
|
494 gSitesStmt.params.limit = this.PLACES_SITES_LIMIT; |
|
495 gSitesStmt.executeAsync({ |
|
496 handleResult: function(aResults) { |
|
497 AboutPermissions.startSitesListBatch(); |
|
498 let row; |
|
499 while (row = aResults.getNextRow()) { |
|
500 let host = row.getResultByName("host"); |
|
501 AboutPermissions.addHost(host); |
|
502 } |
|
503 AboutPermissions.endSitesListBatch(); |
|
504 }, |
|
505 handleError: function(aError) { |
|
506 Cu.reportError("AboutPermissions: " + aError); |
|
507 }, |
|
508 handleCompletion: function(aReason) { |
|
509 // Notify oberservers for testing purposes. |
|
510 AboutPermissions._initPlacesDone = true; |
|
511 if (AboutPermissions._initServicesDone) { |
|
512 Services.obs.notifyObservers(null, "browser-permissions-initialized", null); |
|
513 } |
|
514 } |
|
515 }); |
|
516 }, |
|
517 |
|
518 /** |
|
519 * Drives getEnumerateServicesGenerator to work in intervals. |
|
520 */ |
|
521 enumerateServicesDriver: function() { |
|
522 if (this.enumerateServicesGenerator.next()) { |
|
523 // Build top sitesList items faster so that the list never seems sparse |
|
524 let delay = Math.min(this.sitesList.itemCount * 5, this.LIST_BUILD_DELAY); |
|
525 setTimeout(this.enumerateServicesDriver.bind(this), delay); |
|
526 } else { |
|
527 this.enumerateServicesGenerator.close(); |
|
528 this._initServicesDone = true; |
|
529 if (this._initPlacesDone) { |
|
530 Services.obs.notifyObservers(null, "browser-permissions-initialized", null); |
|
531 } |
|
532 } |
|
533 }, |
|
534 |
|
535 /** |
|
536 * Finds sites that have non-default permissions and creates Site objects for |
|
537 * them if they are not already stored in _sites. |
|
538 */ |
|
539 getEnumerateServicesGenerator: function() { |
|
540 let itemCnt = 1; |
|
541 |
|
542 let logins = Services.logins.getAllLogins(); |
|
543 logins.forEach(function(aLogin) { |
|
544 if (itemCnt % this.LIST_BUILD_CHUNK == 0) { |
|
545 yield true; |
|
546 } |
|
547 try { |
|
548 // aLogin.hostname is a string in origin URL format (e.g. "http://foo.com") |
|
549 let uri = NetUtil.newURI(aLogin.hostname); |
|
550 this.addHost(uri.host); |
|
551 } catch (e) { |
|
552 // newURI will throw for add-ons logins stored in chrome:// URIs |
|
553 } |
|
554 itemCnt++; |
|
555 }, this); |
|
556 |
|
557 let disabledHosts = Services.logins.getAllDisabledHosts(); |
|
558 disabledHosts.forEach(function(aHostname) { |
|
559 if (itemCnt % this.LIST_BUILD_CHUNK == 0) { |
|
560 yield true; |
|
561 } |
|
562 try { |
|
563 // aHostname is a string in origin URL format (e.g. "http://foo.com") |
|
564 let uri = NetUtil.newURI(aHostname); |
|
565 this.addHost(uri.host); |
|
566 } catch (e) { |
|
567 // newURI will throw for add-ons logins stored in chrome:// URIs |
|
568 } |
|
569 itemCnt++; |
|
570 }, this); |
|
571 |
|
572 let (enumerator = Services.perms.enumerator) { |
|
573 while (enumerator.hasMoreElements()) { |
|
574 if (itemCnt % this.LIST_BUILD_CHUNK == 0) { |
|
575 yield true; |
|
576 } |
|
577 let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission); |
|
578 // Only include sites with exceptions set for supported permission types. |
|
579 if (this._supportedPermissions.indexOf(permission.type) != -1) { |
|
580 this.addHost(permission.host); |
|
581 } |
|
582 itemCnt++; |
|
583 } |
|
584 } |
|
585 |
|
586 yield false; |
|
587 }, |
|
588 |
|
589 /** |
|
590 * Creates a new Site and adds it to _sites if it's not already there. |
|
591 * |
|
592 * @param aHost |
|
593 * A host string. |
|
594 */ |
|
595 addHost: function(aHost) { |
|
596 if (aHost in this._sites) { |
|
597 return; |
|
598 } |
|
599 let site = new Site(aHost); |
|
600 this._sites[aHost] = site; |
|
601 this.addToSitesList(site); |
|
602 }, |
|
603 |
|
604 /** |
|
605 * Populates sites-list richlistbox with data from Site object. |
|
606 * |
|
607 * @param aSite |
|
608 * A Site object. |
|
609 */ |
|
610 addToSitesList: function(aSite) { |
|
611 let item = document.createElement("richlistitem"); |
|
612 item.setAttribute("class", "site"); |
|
613 item.setAttribute("value", aSite.host); |
|
614 |
|
615 aSite.getFavicon(function(aURL) { |
|
616 item.setAttribute("favicon", aURL); |
|
617 }); |
|
618 aSite.listitem = item; |
|
619 |
|
620 // Make sure to only display relevant items when list is filtered |
|
621 let filterValue = document.getElementById("sites-filter").value.toLowerCase(); |
|
622 item.collapsed = aSite.host.toLowerCase().indexOf(filterValue) == -1; |
|
623 |
|
624 (this._listFragment || this.sitesList).appendChild(item); |
|
625 }, |
|
626 |
|
627 startSitesListBatch: function () { |
|
628 if (!this._listFragment) |
|
629 this._listFragment = document.createDocumentFragment(); |
|
630 }, |
|
631 |
|
632 endSitesListBatch: function () { |
|
633 if (this._listFragment) { |
|
634 this.sitesList.appendChild(this._listFragment); |
|
635 this._listFragment = null; |
|
636 } |
|
637 }, |
|
638 |
|
639 /** |
|
640 * Hides sites in richlistbox based on search text in sites-filter textbox. |
|
641 */ |
|
642 filterSitesList: function() { |
|
643 let siteItems = this.sitesList.children; |
|
644 let filterValue = document.getElementById("sites-filter").value.toLowerCase(); |
|
645 |
|
646 if (filterValue == "") { |
|
647 for (let i = 0; i < siteItems.length; i++) { |
|
648 siteItems[i].collapsed = false; |
|
649 } |
|
650 return; |
|
651 } |
|
652 |
|
653 for (let i = 0; i < siteItems.length; i++) { |
|
654 let siteValue = siteItems[i].value.toLowerCase(); |
|
655 siteItems[i].collapsed = siteValue.indexOf(filterValue) == -1; |
|
656 } |
|
657 }, |
|
658 |
|
659 /** |
|
660 * Removes all evidence of the selected site. The "forget this site" observer |
|
661 * will call deleteFromSitesList to update the UI. |
|
662 */ |
|
663 forgetSite: function() { |
|
664 this._selectedSite.forgetSite(); |
|
665 }, |
|
666 |
|
667 /** |
|
668 * Deletes sites for a host and all of its sub-domains. Removes these sites |
|
669 * from _sites and removes their corresponding elements from the DOM. |
|
670 * |
|
671 * @param aHost |
|
672 * The host string corresponding to the site to delete. |
|
673 */ |
|
674 deleteFromSitesList: function(aHost) { |
|
675 for each (let site in this._sites) { |
|
676 if (site.host.hasRootDomain(aHost)) { |
|
677 if (site == this._selectedSite) { |
|
678 // Replace site-specific interface with "All Sites" interface. |
|
679 this.sitesList.selectedItem = document.getElementById("all-sites-item"); |
|
680 } |
|
681 |
|
682 this.sitesList.removeChild(site.listitem); |
|
683 delete this._sites[site.host]; |
|
684 } |
|
685 } |
|
686 }, |
|
687 |
|
688 /** |
|
689 * Shows interface for managing site-specific permissions. |
|
690 */ |
|
691 onSitesListSelect: function(event) { |
|
692 if (event.target.selectedItem.id == "all-sites-item") { |
|
693 // Clear the header label value from the previously selected site. |
|
694 document.getElementById("site-label").value = ""; |
|
695 this.manageDefaultPermissions(); |
|
696 return; |
|
697 } |
|
698 |
|
699 let host = event.target.value; |
|
700 let site = this._selectedSite = this._sites[host]; |
|
701 document.getElementById("site-label").value = host; |
|
702 document.getElementById("header-deck").selectedPanel = |
|
703 document.getElementById("site-header"); |
|
704 |
|
705 this.updateVisitCount(); |
|
706 this.updatePermissionsBox(); |
|
707 }, |
|
708 |
|
709 /** |
|
710 * Shows interface for managing default permissions. This corresponds to |
|
711 * the "All Sites" list item. |
|
712 */ |
|
713 manageDefaultPermissions: function() { |
|
714 this._selectedSite = null; |
|
715 |
|
716 document.getElementById("header-deck").selectedPanel = |
|
717 document.getElementById("defaults-header"); |
|
718 |
|
719 this.updatePermissionsBox(); |
|
720 }, |
|
721 |
|
722 /** |
|
723 * Updates permissions interface based on selected site. |
|
724 */ |
|
725 updatePermissionsBox: function() { |
|
726 this._supportedPermissions.forEach(function(aType){ |
|
727 this.updatePermission(aType); |
|
728 }, this); |
|
729 |
|
730 this.updatePasswordsCount(); |
|
731 this.updateCookiesCount(); |
|
732 }, |
|
733 |
|
734 /** |
|
735 * Sets menulist for a given permission to the correct state, based on the |
|
736 * stored permission. |
|
737 * |
|
738 * @param aType |
|
739 * The permission type string stored in permission manager. |
|
740 * e.g. "cookie", "geo", "indexedDB", "popup", "image" |
|
741 */ |
|
742 updatePermission: function(aType) { |
|
743 let allowItem = document.getElementById(aType + "-" + PermissionDefaults.ALLOW); |
|
744 allowItem.hidden = !this._selectedSite && |
|
745 this._noGlobalAllow.indexOf(aType) != -1; |
|
746 let denyItem = document.getElementById(aType + "-" + PermissionDefaults.DENY); |
|
747 denyItem.hidden = !this._selectedSite && |
|
748 this._noGlobalDeny.indexOf(aType) != -1; |
|
749 |
|
750 let permissionMenulist = document.getElementById(aType + "-menulist"); |
|
751 let permissionValue; |
|
752 if (!this._selectedSite) { |
|
753 // If there is no selected site, we are updating the default permissions interface. |
|
754 permissionValue = PermissionDefaults[aType]; |
|
755 if (aType == "cookie") |
|
756 // cookie-9 corresponds to ALLOW_FIRST_PARTY_ONLY, which is reserved |
|
757 // for site-specific preferences only. |
|
758 document.getElementById("cookie-9").hidden = true; |
|
759 } else { |
|
760 if (aType == "cookie") |
|
761 document.getElementById("cookie-9").hidden = false; |
|
762 let result = {}; |
|
763 permissionValue = this._selectedSite.getPermission(aType, result) ? |
|
764 result.value : PermissionDefaults[aType]; |
|
765 } |
|
766 |
|
767 permissionMenulist.selectedItem = document.getElementById(aType + "-" + permissionValue); |
|
768 }, |
|
769 |
|
770 onPermissionCommand: function(event) { |
|
771 let permissionType = event.currentTarget.getAttribute("type"); |
|
772 let permissionValue = event.target.value; |
|
773 |
|
774 if (!this._selectedSite) { |
|
775 // If there is no selected site, we are setting the default permission. |
|
776 PermissionDefaults[permissionType] = permissionValue; |
|
777 } else { |
|
778 this._selectedSite.setPermission(permissionType, permissionValue); |
|
779 } |
|
780 }, |
|
781 |
|
782 updateVisitCount: function() { |
|
783 this._selectedSite.getVisitCount(function(aCount) { |
|
784 let visitForm = AboutPermissions._stringBundle.GetStringFromName("visitCount"); |
|
785 let visitLabel = PluralForm.get(aCount, visitForm) |
|
786 .replace("#1", aCount); |
|
787 document.getElementById("site-visit-count").value = visitLabel; |
|
788 }); |
|
789 }, |
|
790 |
|
791 updatePasswordsCount: function() { |
|
792 if (!this._selectedSite) { |
|
793 document.getElementById("passwords-count").hidden = true; |
|
794 document.getElementById("passwords-manage-all-button").hidden = false; |
|
795 return; |
|
796 } |
|
797 |
|
798 let passwordsCount = this._selectedSite.logins.length; |
|
799 let passwordsForm = this._stringBundle.GetStringFromName("passwordsCount"); |
|
800 let passwordsLabel = PluralForm.get(passwordsCount, passwordsForm) |
|
801 .replace("#1", passwordsCount); |
|
802 |
|
803 document.getElementById("passwords-label").value = passwordsLabel; |
|
804 document.getElementById("passwords-manage-button").disabled = (passwordsCount < 1); |
|
805 document.getElementById("passwords-manage-all-button").hidden = true; |
|
806 document.getElementById("passwords-count").hidden = false; |
|
807 }, |
|
808 |
|
809 /** |
|
810 * Opens password manager dialog. |
|
811 */ |
|
812 managePasswords: function() { |
|
813 let selectedHost = ""; |
|
814 if (this._selectedSite) { |
|
815 selectedHost = this._selectedSite.host; |
|
816 } |
|
817 |
|
818 let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager"); |
|
819 if (win) { |
|
820 win.setFilter(selectedHost); |
|
821 win.focus(); |
|
822 } else { |
|
823 window.openDialog("chrome://passwordmgr/content/passwordManager.xul", |
|
824 "Toolkit:PasswordManager", "", {filterString : selectedHost}); |
|
825 } |
|
826 }, |
|
827 |
|
828 updateCookiesCount: function() { |
|
829 if (!this._selectedSite) { |
|
830 document.getElementById("cookies-count").hidden = true; |
|
831 document.getElementById("cookies-clear-all-button").hidden = false; |
|
832 document.getElementById("cookies-manage-all-button").hidden = false; |
|
833 return; |
|
834 } |
|
835 |
|
836 let cookiesCount = this._selectedSite.cookies.length; |
|
837 let cookiesForm = this._stringBundle.GetStringFromName("cookiesCount"); |
|
838 let cookiesLabel = PluralForm.get(cookiesCount, cookiesForm) |
|
839 .replace("#1", cookiesCount); |
|
840 |
|
841 document.getElementById("cookies-label").value = cookiesLabel; |
|
842 document.getElementById("cookies-clear-button").disabled = (cookiesCount < 1); |
|
843 document.getElementById("cookies-manage-button").disabled = (cookiesCount < 1); |
|
844 document.getElementById("cookies-clear-all-button").hidden = true; |
|
845 document.getElementById("cookies-manage-all-button").hidden = true; |
|
846 document.getElementById("cookies-count").hidden = false; |
|
847 }, |
|
848 |
|
849 /** |
|
850 * Clears cookies for the selected site. |
|
851 */ |
|
852 clearCookies: function() { |
|
853 if (!this._selectedSite) { |
|
854 return; |
|
855 } |
|
856 let site = this._selectedSite; |
|
857 site.clearCookies(site.cookies); |
|
858 this.updateCookiesCount(); |
|
859 }, |
|
860 |
|
861 /** |
|
862 * Opens cookie manager dialog. |
|
863 */ |
|
864 manageCookies: function() { |
|
865 let selectedHost = ""; |
|
866 if (this._selectedSite) { |
|
867 selectedHost = this._selectedSite.host; |
|
868 } |
|
869 |
|
870 let win = Services.wm.getMostRecentWindow("Browser:Cookies"); |
|
871 if (win) { |
|
872 win.gCookiesWindow.setFilter(selectedHost); |
|
873 win.focus(); |
|
874 } else { |
|
875 window.openDialog("chrome://browser/content/preferences/cookies.xul", |
|
876 "Browser:Cookies", "", {filterString : selectedHost}); |
|
877 } |
|
878 } |
|
879 } |
|
880 |
|
881 // See nsPrivateBrowsingService.js |
|
882 String.prototype.hasRootDomain = function hasRootDomain(aDomain) { |
|
883 let index = this.indexOf(aDomain); |
|
884 if (index == -1) |
|
885 return false; |
|
886 |
|
887 if (this == aDomain) |
|
888 return true; |
|
889 |
|
890 let prevChar = this[index - 1]; |
|
891 return (index == (this.length - aDomain.length)) && |
|
892 (prevChar == "." || prevChar == "/"); |
|
893 } |