browser/components/preferences/aboutPermissions.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/preferences/aboutPermissions.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,893 @@
     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
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +let Ci = Components.interfaces;
     1.9 +let Cc = Components.classes;
    1.10 +let Cu = Components.utils;
    1.11 +
    1.12 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.13 +Cu.import("resource://gre/modules/Services.jsm");
    1.14 +Cu.import("resource://gre/modules/DownloadUtils.jsm");
    1.15 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.16 +Cu.import("resource://gre/modules/ForgetAboutSite.jsm");
    1.17 +
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    1.19 +                                  "resource://gre/modules/PluralForm.jsm");
    1.20 +
    1.21 +let gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
    1.22 +                      getService(Ci.nsIFaviconService);
    1.23 +
    1.24 +let gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"].
    1.25 +                      getService(Ci.nsPIPlacesDatabase).
    1.26 +                      DBConnection.
    1.27 +                      clone(true);
    1.28 +
    1.29 +let gSitesStmt = gPlacesDatabase.createAsyncStatement(
    1.30 +                  "SELECT get_unreversed_host(rev_host) AS host " +
    1.31 +                  "FROM moz_places " +
    1.32 +                  "WHERE rev_host > '.' " +
    1.33 +                  "AND visit_count > 0 " +
    1.34 +                  "GROUP BY rev_host " +
    1.35 +                  "ORDER BY MAX(frecency) DESC " +
    1.36 +                  "LIMIT :limit");
    1.37 +
    1.38 +let gVisitStmt = gPlacesDatabase.createAsyncStatement(
    1.39 +                  "SELECT SUM(visit_count) AS count " +
    1.40 +                  "FROM moz_places " +
    1.41 +                  "WHERE rev_host = :rev_host");
    1.42 +
    1.43 +/**
    1.44 + * Permission types that should be tested with testExactPermission, as opposed
    1.45 + * to testPermission. This is based on what consumers use to test these permissions.
    1.46 + */
    1.47 +let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"];
    1.48 +
    1.49 +/**
    1.50 + * Site object represents a single site, uniquely identified by a host.
    1.51 + */
    1.52 +function Site(host) {
    1.53 +  this.host = host;
    1.54 +  this.listitem = null;
    1.55 +
    1.56 +  this.httpURI = NetUtil.newURI("http://" + this.host);
    1.57 +  this.httpsURI = NetUtil.newURI("https://" + this.host);
    1.58 +}
    1.59 +
    1.60 +Site.prototype = {
    1.61 +  /**
    1.62 +   * Gets the favicon to use for the site. The callback only gets called if
    1.63 +   * a favicon is found for either the http URI or the https URI.
    1.64 +   *
    1.65 +   * @param aCallback
    1.66 +   *        A callback function that takes a favicon image URL as a parameter.
    1.67 +   */
    1.68 +  getFavicon: function Site_getFavicon(aCallback) {
    1.69 +    function invokeCallback(aFaviconURI) {
    1.70 +      try {
    1.71 +        // Use getFaviconLinkForIcon to get image data from the database instead
    1.72 +        // of using the favicon URI to fetch image data over the network.
    1.73 +        aCallback(gFaviconService.getFaviconLinkForIcon(aFaviconURI).spec);
    1.74 +      } catch (e) {
    1.75 +        Cu.reportError("AboutPermissions: " + e);
    1.76 +      }
    1.77 +    }
    1.78 +
    1.79 +    // Try to find favicon for both URIs, but always prefer the https favicon.
    1.80 +    gFaviconService.getFaviconURLForPage(this.httpsURI, function (aURI) {
    1.81 +      if (aURI) {
    1.82 +        invokeCallback(aURI);
    1.83 +      } else {
    1.84 +        gFaviconService.getFaviconURLForPage(this.httpURI, function (aURI) {
    1.85 +          if (aURI) {
    1.86 +            invokeCallback(aURI);
    1.87 +          }
    1.88 +        });
    1.89 +      }
    1.90 +    }.bind(this));
    1.91 +  },
    1.92 +
    1.93 +  /**
    1.94 +   * Gets the number of history visits for the site.
    1.95 +   *
    1.96 +   * @param aCallback
    1.97 +   *        A function that takes the visit count (a number) as a parameter.
    1.98 +   */
    1.99 +  getVisitCount: function Site_getVisitCount(aCallback) {
   1.100 +    let rev_host = this.host.split("").reverse().join("") + ".";
   1.101 +    gVisitStmt.params.rev_host = rev_host;
   1.102 +    gVisitStmt.executeAsync({
   1.103 +      handleResult: function(aResults) {
   1.104 +        let row = aResults.getNextRow();
   1.105 +        let count = row.getResultByName("count") || 0;
   1.106 +        try {
   1.107 +          aCallback(count);
   1.108 +        } catch (e) {
   1.109 +          Cu.reportError("AboutPermissions: " + e);
   1.110 +        }
   1.111 +      },
   1.112 +      handleError: function(aError) {
   1.113 +        Cu.reportError("AboutPermissions: " + aError);
   1.114 +      },
   1.115 +      handleCompletion: function(aReason) {
   1.116 +      }
   1.117 +    });
   1.118 +  },
   1.119 +
   1.120 +  /**
   1.121 +   * Gets the permission value stored for a specified permission type.
   1.122 +   *
   1.123 +   * @param aType
   1.124 +   *        The permission type string stored in permission manager.
   1.125 +   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
   1.126 +   * @param aResultObj
   1.127 +   *        An object that stores the permission value set for aType.
   1.128 +   *
   1.129 +   * @return A boolean indicating whether or not a permission is set.
   1.130 +   */
   1.131 +  getPermission: function Site_getPermission(aType, aResultObj) {
   1.132 +    // Password saving isn't a nsIPermissionManager permission type, so handle
   1.133 +    // it seperately.
   1.134 +    if (aType == "password") {
   1.135 +      aResultObj.value =  this.loginSavingEnabled ?
   1.136 +                          Ci.nsIPermissionManager.ALLOW_ACTION :
   1.137 +                          Ci.nsIPermissionManager.DENY_ACTION;
   1.138 +      return true;
   1.139 +    }
   1.140 +
   1.141 +    let permissionValue;
   1.142 +    if (TEST_EXACT_PERM_TYPES.indexOf(aType) == -1) {
   1.143 +      permissionValue = Services.perms.testPermission(this.httpURI, aType);
   1.144 +    } else {
   1.145 +      permissionValue = Services.perms.testExactPermission(this.httpURI, aType);
   1.146 +    }
   1.147 +    aResultObj.value = permissionValue;
   1.148 +
   1.149 +    return permissionValue != Ci.nsIPermissionManager.UNKNOWN_ACTION;
   1.150 +  },
   1.151 +
   1.152 +  /**
   1.153 +   * Sets a permission for the site given a permission type and value.
   1.154 +   *
   1.155 +   * @param aType
   1.156 +   *        The permission type string stored in permission manager.
   1.157 +   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
   1.158 +   * @param aPerm
   1.159 +   *        The permission value to set for the permission type. This should
   1.160 +   *        be one of the constants defined in nsIPermissionManager.
   1.161 +   */
   1.162 +  setPermission: function Site_setPermission(aType, aPerm) {
   1.163 +    // Password saving isn't a nsIPermissionManager permission type, so handle
   1.164 +    // it seperately.
   1.165 +    if (aType == "password") {
   1.166 +      this.loginSavingEnabled = aPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
   1.167 +      return;
   1.168 +    }
   1.169 +
   1.170 +    // Using httpURI is kind of bogus, but the permission manager stores the
   1.171 +    // permission for the host, so the right thing happens in the end.
   1.172 +    Services.perms.add(this.httpURI, aType, aPerm);
   1.173 +  },
   1.174 +
   1.175 +  /**
   1.176 +   * Clears a user-set permission value for the site given a permission type.
   1.177 +   *
   1.178 +   * @param aType
   1.179 +   *        The permission type string stored in permission manager.
   1.180 +   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
   1.181 +   */
   1.182 +  clearPermission: function Site_clearPermission(aType) {
   1.183 +    Services.perms.remove(this.host, aType);
   1.184 +  },
   1.185 +
   1.186 +  /**
   1.187 +   * Gets cookies stored for the site. This does not return cookies stored
   1.188 +   * for the base domain, only the exact hostname stored for the site.
   1.189 +   *
   1.190 +   * @return An array of the cookies set for the site.
   1.191 +   */
   1.192 +  get cookies() {
   1.193 +    let cookies = [];
   1.194 +    let enumerator = Services.cookies.getCookiesFromHost(this.host);
   1.195 +    while (enumerator.hasMoreElements()) {
   1.196 +      let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
   1.197 +      // getCookiesFromHost returns cookies for base domain, but we only want
   1.198 +      // the cookies for the exact domain.
   1.199 +      if (cookie.rawHost == this.host) {
   1.200 +        cookies.push(cookie);
   1.201 +      }
   1.202 +    }
   1.203 +    return cookies;
   1.204 +  },
   1.205 +
   1.206 +  /**
   1.207 +   * Removes a set of specific cookies from the browser.
   1.208 +   */
   1.209 +  clearCookies: function Site_clearCookies() {
   1.210 +    this.cookies.forEach(function(aCookie) {
   1.211 +      Services.cookies.remove(aCookie.host, aCookie.name, aCookie.path, false);
   1.212 +    });
   1.213 +  },
   1.214 +
   1.215 +  /**
   1.216 +   * Gets logins stored for the site.
   1.217 +   *
   1.218 +   * @return An array of the logins stored for the site.
   1.219 +   */
   1.220 +  get logins() {
   1.221 +    let httpLogins = Services.logins.findLogins({}, this.httpURI.prePath, "", "");
   1.222 +    let httpsLogins = Services.logins.findLogins({}, this.httpsURI.prePath, "", "");
   1.223 +    return httpLogins.concat(httpsLogins);
   1.224 +  },
   1.225 +
   1.226 +  get loginSavingEnabled() {
   1.227 +    // Only say that login saving is blocked if it is blocked for both http and https.
   1.228 +    return Services.logins.getLoginSavingEnabled(this.httpURI.prePath) &&
   1.229 +           Services.logins.getLoginSavingEnabled(this.httpsURI.prePath);
   1.230 +  },
   1.231 +
   1.232 +  set loginSavingEnabled(isEnabled) {
   1.233 +    Services.logins.setLoginSavingEnabled(this.httpURI.prePath, isEnabled);
   1.234 +    Services.logins.setLoginSavingEnabled(this.httpsURI.prePath, isEnabled);
   1.235 +  },
   1.236 +
   1.237 +  /**
   1.238 +   * Removes all data from the browser corresponding to the site.
   1.239 +   */
   1.240 +  forgetSite: function Site_forgetSite() {
   1.241 +    ForgetAboutSite.removeDataFromDomain(this.host);
   1.242 +  }
   1.243 +}
   1.244 +
   1.245 +/**
   1.246 + * PermissionDefaults object keeps track of default permissions for sites based
   1.247 + * on global preferences.
   1.248 + *
   1.249 + * Inspired by pageinfo/permissions.js
   1.250 + */
   1.251 +let PermissionDefaults = {
   1.252 +  UNKNOWN: Ci.nsIPermissionManager.UNKNOWN_ACTION, // 0
   1.253 +  ALLOW: Ci.nsIPermissionManager.ALLOW_ACTION, // 1
   1.254 +  DENY: Ci.nsIPermissionManager.DENY_ACTION, // 2
   1.255 +  SESSION: Ci.nsICookiePermission.ACCESS_SESSION, // 8
   1.256 +
   1.257 +  get password() {
   1.258 +    if (Services.prefs.getBoolPref("signon.rememberSignons")) {
   1.259 +      return this.ALLOW;
   1.260 +    }
   1.261 +    return this.DENY;
   1.262 +  },
   1.263 +  set password(aValue) {
   1.264 +    let value = (aValue != this.DENY);
   1.265 +    Services.prefs.setBoolPref("signon.rememberSignons", value);
   1.266 +  },
   1.267 +
   1.268 +  // For use with network.cookie.* prefs.
   1.269 +  COOKIE_ACCEPT: 0,
   1.270 +  COOKIE_DENY: 2,
   1.271 +  COOKIE_NORMAL: 0,
   1.272 +  COOKIE_SESSION: 2,
   1.273 +
   1.274 +  get cookie() {
   1.275 +    if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == this.COOKIE_DENY) {
   1.276 +      return this.DENY;
   1.277 +    }
   1.278 +
   1.279 +    if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == this.COOKIE_SESSION) {
   1.280 +      return this.SESSION;
   1.281 +    }
   1.282 +    return this.ALLOW;
   1.283 +  },
   1.284 +  set cookie(aValue) {
   1.285 +    let value = (aValue == this.DENY) ? this.COOKIE_DENY : this.COOKIE_ACCEPT;
   1.286 +    Services.prefs.setIntPref("network.cookie.cookieBehavior", value);
   1.287 +
   1.288 +    let lifetimeValue = aValue == this.SESSION ? this.COOKIE_SESSION :
   1.289 +                                                 this.COOKIE_NORMAL;
   1.290 +    Services.prefs.setIntPref("network.cookie.lifetimePolicy", lifetimeValue);
   1.291 +  },
   1.292 +
   1.293 +  get geo() {
   1.294 +    if (!Services.prefs.getBoolPref("geo.enabled")) {
   1.295 +      return this.DENY;
   1.296 +    }
   1.297 +    // We always ask for permission to share location with a specific site, so
   1.298 +    // there is no global ALLOW.
   1.299 +    return this.UNKNOWN;
   1.300 +  },
   1.301 +  set geo(aValue) {
   1.302 +    let value = (aValue != this.DENY);
   1.303 +    Services.prefs.setBoolPref("geo.enabled", value);
   1.304 +  },
   1.305 +
   1.306 +  get indexedDB() {
   1.307 +    if (!Services.prefs.getBoolPref("dom.indexedDB.enabled")) {
   1.308 +      return this.DENY;
   1.309 +    }
   1.310 +    // We always ask for permission to enable indexedDB storage for a specific
   1.311 +    // site, so there is no global ALLOW.
   1.312 +    return this.UNKNOWN;
   1.313 +  },
   1.314 +  set indexedDB(aValue) {
   1.315 +    let value = (aValue != this.DENY);
   1.316 +    Services.prefs.setBoolPref("dom.indexedDB.enabled", value);
   1.317 +  },
   1.318 +
   1.319 +  get popup() {
   1.320 +    if (Services.prefs.getBoolPref("dom.disable_open_during_load")) {
   1.321 +      return this.DENY;
   1.322 +    }
   1.323 +    return this.ALLOW;
   1.324 +  },
   1.325 +  set popup(aValue) {
   1.326 +    let value = (aValue == this.DENY);
   1.327 +    Services.prefs.setBoolPref("dom.disable_open_during_load", value);
   1.328 +  },
   1.329 +
   1.330 +  get fullscreen() {
   1.331 +    if (!Services.prefs.getBoolPref("full-screen-api.enabled")) {
   1.332 +      return this.DENY;
   1.333 +    }
   1.334 +    return this.UNKNOWN;
   1.335 +  },
   1.336 +  set fullscreen(aValue) {
   1.337 +    let value = (aValue != this.DENY);
   1.338 +    Services.prefs.setBoolPref("full-screen-api.enabled", value);
   1.339 +  },
   1.340 +
   1.341 +  get camera() this.UNKNOWN,
   1.342 +  get microphone() this.UNKNOWN
   1.343 +};
   1.344 +
   1.345 +/**
   1.346 + * AboutPermissions manages the about:permissions page.
   1.347 + */
   1.348 +let AboutPermissions = {
   1.349 +  /**
   1.350 +   * Number of sites to return from the places database.
   1.351 +   */
   1.352 +  PLACES_SITES_LIMIT: 50,
   1.353 +
   1.354 +  /**
   1.355 +   * When adding sites to the dom sites-list, divide workload into intervals.
   1.356 +   */
   1.357 +  LIST_BUILD_CHUNK: 5, // interval size
   1.358 +  LIST_BUILD_DELAY: 100, // delay between intervals
   1.359 +
   1.360 +  /**
   1.361 +   * Stores a mapping of host strings to Site objects.
   1.362 +   */
   1.363 +  _sites: {},
   1.364 +
   1.365 +  sitesList: null,
   1.366 +  _selectedSite: null,
   1.367 +
   1.368 +  /**
   1.369 +   * For testing, track initializations so we can send notifications
   1.370 +   */
   1.371 +  _initPlacesDone: false,
   1.372 +  _initServicesDone: false,
   1.373 +
   1.374 +  /**
   1.375 +   * This reflects the permissions that we expose in the UI. These correspond
   1.376 +   * to permission type strings in the permission manager, PermissionDefaults,
   1.377 +   * and element ids in aboutPermissions.xul.
   1.378 +   *
   1.379 +   * Potential future additions: "sts/use", "sts/subd"
   1.380 +   */
   1.381 +  _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup",
   1.382 +                          "fullscreen", "camera", "microphone"],
   1.383 +
   1.384 +  /**
   1.385 +   * Permissions that don't have a global "Allow" option.
   1.386 +   */
   1.387 +  _noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"],
   1.388 +
   1.389 +  /**
   1.390 +   * Permissions that don't have a global "Deny" option.
   1.391 +   */
   1.392 +  _noGlobalDeny: ["camera", "microphone"],
   1.393 +
   1.394 +  _stringBundle: Services.strings.
   1.395 +                 createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
   1.396 +
   1.397 +  /**
   1.398 +   * Called on page load.
   1.399 +   */
   1.400 +  init: function() {
   1.401 +    this.sitesList = document.getElementById("sites-list");
   1.402 +
   1.403 +    this.getSitesFromPlaces();
   1.404 +
   1.405 +    this.enumerateServicesGenerator = this.getEnumerateServicesGenerator();
   1.406 +    setTimeout(this.enumerateServicesDriver.bind(this), this.LIST_BUILD_DELAY);
   1.407 +
   1.408 +    // Attach observers in case data changes while the page is open.
   1.409 +    Services.prefs.addObserver("signon.rememberSignons", this, false);
   1.410 +    Services.prefs.addObserver("network.cookie.", this, false);
   1.411 +    Services.prefs.addObserver("geo.enabled", this, false);
   1.412 +    Services.prefs.addObserver("dom.indexedDB.enabled", this, false);
   1.413 +    Services.prefs.addObserver("dom.disable_open_during_load", this, false);
   1.414 +    Services.prefs.addObserver("full-screen-api.enabled", this, false);
   1.415 +
   1.416 +    Services.obs.addObserver(this, "perm-changed", false);
   1.417 +    Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
   1.418 +    Services.obs.addObserver(this, "cookie-changed", false);
   1.419 +    Services.obs.addObserver(this, "browser:purge-domain-data", false);
   1.420 +
   1.421 +    this._observersInitialized = true;
   1.422 +    Services.obs.notifyObservers(null, "browser-permissions-preinit", null);
   1.423 +  },
   1.424 +
   1.425 +  /**
   1.426 +   * Called on page unload.
   1.427 +   */
   1.428 +  cleanUp: function() {
   1.429 +    if (this._observersInitialized) {
   1.430 +      Services.prefs.removeObserver("signon.rememberSignons", this, false);
   1.431 +      Services.prefs.removeObserver("network.cookie.", this, false);
   1.432 +      Services.prefs.removeObserver("geo.enabled", this, false);
   1.433 +      Services.prefs.removeObserver("dom.indexedDB.enabled", this, false);
   1.434 +      Services.prefs.removeObserver("dom.disable_open_during_load", this, false);
   1.435 +      Services.prefs.removeObserver("full-screen-api.enabled", this, false);
   1.436 +
   1.437 +      Services.obs.removeObserver(this, "perm-changed");
   1.438 +      Services.obs.removeObserver(this, "passwordmgr-storage-changed");
   1.439 +      Services.obs.removeObserver(this, "cookie-changed");
   1.440 +      Services.obs.removeObserver(this, "browser:purge-domain-data");
   1.441 +    }
   1.442 +
   1.443 +    gSitesStmt.finalize();
   1.444 +    gVisitStmt.finalize();
   1.445 +    gPlacesDatabase.asyncClose(null);
   1.446 +  },
   1.447 +
   1.448 +  observe: function (aSubject, aTopic, aData) {
   1.449 +    switch(aTopic) {
   1.450 +      case "perm-changed":
   1.451 +        // Permissions changes only affect individual sites.
   1.452 +        if (!this._selectedSite) {
   1.453 +          break;
   1.454 +        }
   1.455 +        // aSubject is null when nsIPermisionManager::removeAll() is called.
   1.456 +        if (!aSubject) {
   1.457 +          this._supportedPermissions.forEach(function(aType){
   1.458 +            this.updatePermission(aType);
   1.459 +          }, this);
   1.460 +          break;
   1.461 +        }
   1.462 +        let permission = aSubject.QueryInterface(Ci.nsIPermission);
   1.463 +        // We can't compare selectedSite.host and permission.host here because
   1.464 +        // we need to handle the case where a parent domain was changed in a
   1.465 +        // way that affects the subdomain.
   1.466 +        if (this._supportedPermissions.indexOf(permission.type) != -1) {
   1.467 +          this.updatePermission(permission.type);
   1.468 +        }
   1.469 +        break;
   1.470 +      case "nsPref:changed":
   1.471 +        this._supportedPermissions.forEach(function(aType){
   1.472 +          this.updatePermission(aType);
   1.473 +        }, this);
   1.474 +        break;
   1.475 +      case "passwordmgr-storage-changed":
   1.476 +        this.updatePermission("password");
   1.477 +        if (this._selectedSite) {
   1.478 +          this.updatePasswordsCount();
   1.479 +        }
   1.480 +        break;
   1.481 +      case "cookie-changed":
   1.482 +        if (this._selectedSite) {
   1.483 +          this.updateCookiesCount();
   1.484 +        }
   1.485 +        break;
   1.486 +      case "browser:purge-domain-data":
   1.487 +        this.deleteFromSitesList(aData);
   1.488 +        break;
   1.489 +    }
   1.490 +  },
   1.491 +
   1.492 +  /**
   1.493 +   * Creates Site objects for the top-frecency sites in the places database and stores
   1.494 +   * them in _sites. The number of sites created is controlled by PLACES_SITES_LIMIT.
   1.495 +   */
   1.496 +  getSitesFromPlaces: function() {
   1.497 +    gSitesStmt.params.limit = this.PLACES_SITES_LIMIT;
   1.498 +    gSitesStmt.executeAsync({
   1.499 +      handleResult: function(aResults) {
   1.500 +        AboutPermissions.startSitesListBatch();
   1.501 +        let row;
   1.502 +        while (row = aResults.getNextRow()) {
   1.503 +          let host = row.getResultByName("host");
   1.504 +          AboutPermissions.addHost(host);
   1.505 +        }
   1.506 +        AboutPermissions.endSitesListBatch();
   1.507 +      },
   1.508 +      handleError: function(aError) {
   1.509 +        Cu.reportError("AboutPermissions: " + aError);
   1.510 +      },
   1.511 +      handleCompletion: function(aReason) {
   1.512 +        // Notify oberservers for testing purposes.
   1.513 +        AboutPermissions._initPlacesDone = true;
   1.514 +        if (AboutPermissions._initServicesDone) {
   1.515 +          Services.obs.notifyObservers(null, "browser-permissions-initialized", null);
   1.516 +        }
   1.517 +      }
   1.518 +    });
   1.519 +  },
   1.520 +
   1.521 +  /**
   1.522 +   * Drives getEnumerateServicesGenerator to work in intervals.
   1.523 +   */
   1.524 +  enumerateServicesDriver: function() {
   1.525 +    if (this.enumerateServicesGenerator.next()) {
   1.526 +      // Build top sitesList items faster so that the list never seems sparse
   1.527 +      let delay = Math.min(this.sitesList.itemCount * 5, this.LIST_BUILD_DELAY);
   1.528 +      setTimeout(this.enumerateServicesDriver.bind(this), delay);
   1.529 +    } else {
   1.530 +      this.enumerateServicesGenerator.close();
   1.531 +      this._initServicesDone = true;
   1.532 +      if (this._initPlacesDone) {
   1.533 +        Services.obs.notifyObservers(null, "browser-permissions-initialized", null);
   1.534 +      }
   1.535 +    }
   1.536 +  },
   1.537 +
   1.538 +  /**
   1.539 +   * Finds sites that have non-default permissions and creates Site objects for
   1.540 +   * them if they are not already stored in _sites.
   1.541 +   */
   1.542 +  getEnumerateServicesGenerator: function() {
   1.543 +    let itemCnt = 1;
   1.544 +
   1.545 +    let logins = Services.logins.getAllLogins();
   1.546 +    logins.forEach(function(aLogin) {
   1.547 +      if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
   1.548 +        yield true;
   1.549 +      }
   1.550 +      try {
   1.551 +        // aLogin.hostname is a string in origin URL format (e.g. "http://foo.com")
   1.552 +        let uri = NetUtil.newURI(aLogin.hostname);
   1.553 +        this.addHost(uri.host);
   1.554 +      } catch (e) {
   1.555 +        // newURI will throw for add-ons logins stored in chrome:// URIs
   1.556 +      }
   1.557 +      itemCnt++;
   1.558 +    }, this);
   1.559 +
   1.560 +    let disabledHosts = Services.logins.getAllDisabledHosts();
   1.561 +    disabledHosts.forEach(function(aHostname) {
   1.562 +      if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
   1.563 +        yield true;
   1.564 +      }
   1.565 +      try {
   1.566 +        // aHostname is a string in origin URL format (e.g. "http://foo.com")
   1.567 +        let uri = NetUtil.newURI(aHostname);
   1.568 +        this.addHost(uri.host);
   1.569 +      } catch (e) {
   1.570 +        // newURI will throw for add-ons logins stored in chrome:// URIs
   1.571 +      }
   1.572 +      itemCnt++;
   1.573 +    }, this);
   1.574 +
   1.575 +    let (enumerator = Services.perms.enumerator) {
   1.576 +      while (enumerator.hasMoreElements()) {
   1.577 +        if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
   1.578 +          yield true;
   1.579 +        }
   1.580 +        let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
   1.581 +        // Only include sites with exceptions set for supported permission types.
   1.582 +        if (this._supportedPermissions.indexOf(permission.type) != -1) {
   1.583 +          this.addHost(permission.host);
   1.584 +        }
   1.585 +        itemCnt++;
   1.586 +      }
   1.587 +    }
   1.588 +
   1.589 +    yield false;
   1.590 +  },
   1.591 +
   1.592 +  /**
   1.593 +   * Creates a new Site and adds it to _sites if it's not already there.
   1.594 +   *
   1.595 +   * @param aHost
   1.596 +   *        A host string.
   1.597 +   */
   1.598 +  addHost: function(aHost) {
   1.599 +    if (aHost in this._sites) {
   1.600 +      return;
   1.601 +    }
   1.602 +    let site = new Site(aHost);
   1.603 +    this._sites[aHost] = site;
   1.604 +    this.addToSitesList(site);
   1.605 +  },
   1.606 +
   1.607 +  /**
   1.608 +   * Populates sites-list richlistbox with data from Site object.
   1.609 +   *
   1.610 +   * @param aSite
   1.611 +   *        A Site object.
   1.612 +   */
   1.613 +  addToSitesList: function(aSite) {
   1.614 +    let item = document.createElement("richlistitem");
   1.615 +    item.setAttribute("class", "site");
   1.616 +    item.setAttribute("value", aSite.host);
   1.617 +
   1.618 +    aSite.getFavicon(function(aURL) {
   1.619 +      item.setAttribute("favicon", aURL);
   1.620 +    });
   1.621 +    aSite.listitem = item;
   1.622 +
   1.623 +    // Make sure to only display relevant items when list is filtered
   1.624 +    let filterValue = document.getElementById("sites-filter").value.toLowerCase();
   1.625 +    item.collapsed = aSite.host.toLowerCase().indexOf(filterValue) == -1;
   1.626 +
   1.627 +    (this._listFragment || this.sitesList).appendChild(item);
   1.628 +  },
   1.629 +
   1.630 +  startSitesListBatch: function () {
   1.631 +    if (!this._listFragment)
   1.632 +      this._listFragment = document.createDocumentFragment();
   1.633 +  },
   1.634 +
   1.635 +  endSitesListBatch: function () {
   1.636 +    if (this._listFragment) {
   1.637 +      this.sitesList.appendChild(this._listFragment);
   1.638 +      this._listFragment = null;
   1.639 +    }
   1.640 +  },
   1.641 +
   1.642 +  /**
   1.643 +   * Hides sites in richlistbox based on search text in sites-filter textbox.
   1.644 +   */
   1.645 +  filterSitesList: function() {
   1.646 +    let siteItems = this.sitesList.children;
   1.647 +    let filterValue = document.getElementById("sites-filter").value.toLowerCase();
   1.648 +
   1.649 +    if (filterValue == "") {
   1.650 +      for (let i = 0; i < siteItems.length; i++) {
   1.651 +        siteItems[i].collapsed = false;
   1.652 +      }
   1.653 +      return;
   1.654 +    }
   1.655 +
   1.656 +    for (let i = 0; i < siteItems.length; i++) {
   1.657 +      let siteValue = siteItems[i].value.toLowerCase();
   1.658 +      siteItems[i].collapsed = siteValue.indexOf(filterValue) == -1;
   1.659 +    }
   1.660 +  },
   1.661 +
   1.662 +  /**
   1.663 +   * Removes all evidence of the selected site. The "forget this site" observer
   1.664 +   * will call deleteFromSitesList to update the UI.
   1.665 +   */
   1.666 +  forgetSite: function() {
   1.667 +    this._selectedSite.forgetSite();
   1.668 +  },
   1.669 +
   1.670 +  /**
   1.671 +   * Deletes sites for a host and all of its sub-domains. Removes these sites
   1.672 +   * from _sites and removes their corresponding elements from the DOM.
   1.673 +   *
   1.674 +   * @param aHost
   1.675 +   *        The host string corresponding to the site to delete.
   1.676 +   */
   1.677 +  deleteFromSitesList: function(aHost) {
   1.678 +    for each (let site in this._sites) {
   1.679 +      if (site.host.hasRootDomain(aHost)) {
   1.680 +        if (site == this._selectedSite) {
   1.681 +          // Replace site-specific interface with "All Sites" interface.
   1.682 +          this.sitesList.selectedItem = document.getElementById("all-sites-item");
   1.683 +        }
   1.684 +
   1.685 +        this.sitesList.removeChild(site.listitem);
   1.686 +        delete this._sites[site.host];
   1.687 +      }
   1.688 +    }
   1.689 +  },
   1.690 +
   1.691 +  /**
   1.692 +   * Shows interface for managing site-specific permissions.
   1.693 +   */
   1.694 +  onSitesListSelect: function(event) {
   1.695 +    if (event.target.selectedItem.id == "all-sites-item") {
   1.696 +      // Clear the header label value from the previously selected site.
   1.697 +      document.getElementById("site-label").value = "";
   1.698 +      this.manageDefaultPermissions();
   1.699 +      return;
   1.700 +    }
   1.701 +
   1.702 +    let host = event.target.value;
   1.703 +    let site = this._selectedSite = this._sites[host];
   1.704 +    document.getElementById("site-label").value = host;
   1.705 +    document.getElementById("header-deck").selectedPanel =
   1.706 +      document.getElementById("site-header");
   1.707 +
   1.708 +    this.updateVisitCount();
   1.709 +    this.updatePermissionsBox();
   1.710 +  },
   1.711 +
   1.712 +  /**
   1.713 +   * Shows interface for managing default permissions. This corresponds to
   1.714 +   * the "All Sites" list item.
   1.715 +   */
   1.716 +  manageDefaultPermissions: function() {
   1.717 +    this._selectedSite = null;
   1.718 +
   1.719 +    document.getElementById("header-deck").selectedPanel =
   1.720 +      document.getElementById("defaults-header");
   1.721 +
   1.722 +    this.updatePermissionsBox();
   1.723 +  },
   1.724 +
   1.725 +  /**
   1.726 +   * Updates permissions interface based on selected site.
   1.727 +   */
   1.728 +  updatePermissionsBox: function() {
   1.729 +    this._supportedPermissions.forEach(function(aType){
   1.730 +      this.updatePermission(aType);
   1.731 +    }, this);
   1.732 +
   1.733 +    this.updatePasswordsCount();
   1.734 +    this.updateCookiesCount();
   1.735 +  },
   1.736 +
   1.737 +  /**
   1.738 +   * Sets menulist for a given permission to the correct state, based on the
   1.739 +   * stored permission.
   1.740 +   *
   1.741 +   * @param aType
   1.742 +   *        The permission type string stored in permission manager.
   1.743 +   *        e.g. "cookie", "geo", "indexedDB", "popup", "image"
   1.744 +   */
   1.745 +  updatePermission: function(aType) {
   1.746 +    let allowItem = document.getElementById(aType + "-" + PermissionDefaults.ALLOW);
   1.747 +    allowItem.hidden = !this._selectedSite &&
   1.748 +                       this._noGlobalAllow.indexOf(aType) != -1;
   1.749 +    let denyItem = document.getElementById(aType + "-" + PermissionDefaults.DENY);
   1.750 +    denyItem.hidden = !this._selectedSite &&
   1.751 +                      this._noGlobalDeny.indexOf(aType) != -1;
   1.752 +
   1.753 +    let permissionMenulist = document.getElementById(aType + "-menulist");
   1.754 +    let permissionValue;
   1.755 +    if (!this._selectedSite) {
   1.756 +      // If there is no selected site, we are updating the default permissions interface.
   1.757 +      permissionValue = PermissionDefaults[aType];
   1.758 +      if (aType == "cookie")
   1.759 +	// cookie-9 corresponds to ALLOW_FIRST_PARTY_ONLY, which is reserved
   1.760 +	// for site-specific preferences only.
   1.761 +	document.getElementById("cookie-9").hidden = true;
   1.762 +    } else {
   1.763 +      if (aType == "cookie")
   1.764 +        document.getElementById("cookie-9").hidden = false;
   1.765 +      let result = {};
   1.766 +      permissionValue = this._selectedSite.getPermission(aType, result) ?
   1.767 +                        result.value : PermissionDefaults[aType];
   1.768 +    }
   1.769 +
   1.770 +    permissionMenulist.selectedItem = document.getElementById(aType + "-" + permissionValue);
   1.771 +  },
   1.772 +
   1.773 +  onPermissionCommand: function(event) {
   1.774 +    let permissionType = event.currentTarget.getAttribute("type");
   1.775 +    let permissionValue = event.target.value;
   1.776 +
   1.777 +    if (!this._selectedSite) {
   1.778 +      // If there is no selected site, we are setting the default permission.
   1.779 +      PermissionDefaults[permissionType] = permissionValue;
   1.780 +    } else {
   1.781 +      this._selectedSite.setPermission(permissionType, permissionValue);
   1.782 +    }
   1.783 +  },
   1.784 +
   1.785 +  updateVisitCount: function() {
   1.786 +    this._selectedSite.getVisitCount(function(aCount) {
   1.787 +      let visitForm = AboutPermissions._stringBundle.GetStringFromName("visitCount");
   1.788 +      let visitLabel = PluralForm.get(aCount, visitForm)
   1.789 +                                  .replace("#1", aCount);
   1.790 +      document.getElementById("site-visit-count").value = visitLabel;
   1.791 +    });
   1.792 +  },
   1.793 +
   1.794 +  updatePasswordsCount: function() {
   1.795 +    if (!this._selectedSite) {
   1.796 +      document.getElementById("passwords-count").hidden = true;
   1.797 +      document.getElementById("passwords-manage-all-button").hidden = false;
   1.798 +      return;
   1.799 +    }
   1.800 +
   1.801 +    let passwordsCount = this._selectedSite.logins.length;
   1.802 +    let passwordsForm = this._stringBundle.GetStringFromName("passwordsCount");
   1.803 +    let passwordsLabel = PluralForm.get(passwordsCount, passwordsForm)
   1.804 +                                   .replace("#1", passwordsCount);
   1.805 +
   1.806 +    document.getElementById("passwords-label").value = passwordsLabel;
   1.807 +    document.getElementById("passwords-manage-button").disabled = (passwordsCount < 1);
   1.808 +    document.getElementById("passwords-manage-all-button").hidden = true;
   1.809 +    document.getElementById("passwords-count").hidden = false;
   1.810 +  },
   1.811 +
   1.812 +  /**
   1.813 +   * Opens password manager dialog.
   1.814 +   */
   1.815 +  managePasswords: function() {
   1.816 +    let selectedHost = "";
   1.817 +    if (this._selectedSite) {
   1.818 +      selectedHost = this._selectedSite.host;
   1.819 +    }
   1.820 +
   1.821 +    let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
   1.822 +    if (win) {
   1.823 +      win.setFilter(selectedHost);
   1.824 +      win.focus();
   1.825 +    } else {
   1.826 +      window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
   1.827 +                        "Toolkit:PasswordManager", "", {filterString : selectedHost});
   1.828 +    }
   1.829 +  },
   1.830 +
   1.831 +  updateCookiesCount: function() {
   1.832 +    if (!this._selectedSite) {
   1.833 +      document.getElementById("cookies-count").hidden = true;
   1.834 +      document.getElementById("cookies-clear-all-button").hidden = false;
   1.835 +      document.getElementById("cookies-manage-all-button").hidden = false;
   1.836 +      return;
   1.837 +    }
   1.838 +
   1.839 +    let cookiesCount = this._selectedSite.cookies.length;
   1.840 +    let cookiesForm = this._stringBundle.GetStringFromName("cookiesCount");
   1.841 +    let cookiesLabel = PluralForm.get(cookiesCount, cookiesForm)
   1.842 +                                 .replace("#1", cookiesCount);
   1.843 +
   1.844 +    document.getElementById("cookies-label").value = cookiesLabel;
   1.845 +    document.getElementById("cookies-clear-button").disabled = (cookiesCount < 1);
   1.846 +    document.getElementById("cookies-manage-button").disabled = (cookiesCount < 1);
   1.847 +    document.getElementById("cookies-clear-all-button").hidden = true;
   1.848 +    document.getElementById("cookies-manage-all-button").hidden = true;
   1.849 +    document.getElementById("cookies-count").hidden = false;
   1.850 +  },
   1.851 +
   1.852 +  /**
   1.853 +   * Clears cookies for the selected site.
   1.854 +   */
   1.855 +  clearCookies: function() {
   1.856 +    if (!this._selectedSite) {
   1.857 +      return;
   1.858 +    }
   1.859 +    let site = this._selectedSite;
   1.860 +    site.clearCookies(site.cookies);
   1.861 +    this.updateCookiesCount();
   1.862 +  },
   1.863 +
   1.864 +  /**
   1.865 +   * Opens cookie manager dialog.
   1.866 +   */
   1.867 +  manageCookies: function() {
   1.868 +    let selectedHost = "";
   1.869 +    if (this._selectedSite) {
   1.870 +      selectedHost = this._selectedSite.host;
   1.871 +    }
   1.872 +
   1.873 +    let win = Services.wm.getMostRecentWindow("Browser:Cookies");
   1.874 +    if (win) {
   1.875 +      win.gCookiesWindow.setFilter(selectedHost);
   1.876 +      win.focus();
   1.877 +    } else {
   1.878 +      window.openDialog("chrome://browser/content/preferences/cookies.xul",
   1.879 +                        "Browser:Cookies", "", {filterString : selectedHost});
   1.880 +    }
   1.881 +  }
   1.882 +}
   1.883 +
   1.884 +// See nsPrivateBrowsingService.js
   1.885 +String.prototype.hasRootDomain = function hasRootDomain(aDomain) {
   1.886 +  let index = this.indexOf(aDomain);
   1.887 +  if (index == -1)
   1.888 +    return false;
   1.889 +
   1.890 +  if (this == aDomain)
   1.891 +    return true;
   1.892 +
   1.893 +  let prevChar = this[index - 1];
   1.894 +  return (index == (this.length - aDomain.length)) &&
   1.895 +         (prevChar == "." || prevChar == "/");
   1.896 +}

mercurial