forked from mirrors/gecko-dev
		
	Only selected items in the permission settings richlistbox should be tabable for accessibility reasons. Differential Revision: https://phabricator.services.mozilla.com/D10079 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			478 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			478 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
/* import-globals-from in-content/extensionControlled.js */
 | 
						|
 | 
						|
ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 | 
						|
ChromeUtils.import("resource:///modules/SitePermissions.jsm");
 | 
						|
 | 
						|
const sitePermissionsL10n = {
 | 
						|
  "desktop-notification": {
 | 
						|
    window: "permissions-site-notification-window",
 | 
						|
    description: "permissions-site-notification-desc",
 | 
						|
    disableLabel: "permissions-site-notification-disable-label",
 | 
						|
    disableDescription: "permissions-site-notification-disable-desc",
 | 
						|
  },
 | 
						|
  "geo": {
 | 
						|
    window: "permissions-site-location-window",
 | 
						|
    description: "permissions-site-location-desc",
 | 
						|
    disableLabel: "permissions-site-location-disable-label",
 | 
						|
    disableDescription: "permissions-site-location-disable-desc",
 | 
						|
  },
 | 
						|
  "camera": {
 | 
						|
    window: "permissions-site-camera-window",
 | 
						|
    description: "permissions-site-camera-desc",
 | 
						|
    disableLabel: "permissions-site-camera-disable-label",
 | 
						|
    disableDescription: "permissions-site-camera-disable-desc",
 | 
						|
  },
 | 
						|
  "microphone": {
 | 
						|
    window: "permissions-site-microphone-window",
 | 
						|
    description: "permissions-site-microphone-desc",
 | 
						|
    disableLabel: "permissions-site-microphone-disable-label",
 | 
						|
    disableDescription: "permissions-site-microphone-disable-desc",
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
function Permission(principal, type, capability, l10nId) {
 | 
						|
  this.principal = principal;
 | 
						|
  this.origin = principal.origin;
 | 
						|
  this.type = type;
 | 
						|
  this.capability = capability;
 | 
						|
  this.l10nId = l10nId;
 | 
						|
}
 | 
						|
 | 
						|
const PERMISSION_STATES = [SitePermissions.ALLOW, SitePermissions.BLOCK, SitePermissions.PROMPT];
 | 
						|
const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
 | 
						|
const NOTIFICATIONS_PERMISSION_PREF = "permissions.default.desktop-notification";
 | 
						|
 | 
						|
var gSitePermissionsManager = {
 | 
						|
  _type: "",
 | 
						|
  _isObserving: false,
 | 
						|
  _permissions: new Map(),
 | 
						|
  _permissionsToChange: new Map(),
 | 
						|
  _permissionsToDelete: new Map(),
 | 
						|
  _list: null,
 | 
						|
  _removeButton: null,
 | 
						|
  _removeAllButton: null,
 | 
						|
  _searchBox: null,
 | 
						|
  _checkbox: null,
 | 
						|
  _currentDefaultPermissionsState: null,
 | 
						|
  _defaultPermissionStatePrefName: null,
 | 
						|
 | 
						|
  onLoad() {
 | 
						|
    let params = window.arguments[0];
 | 
						|
    document.mozSubdialogReady = this.init(params);
 | 
						|
  },
 | 
						|
 | 
						|
  async init(params) {
 | 
						|
    if (!this._isObserving) {
 | 
						|
      Services.obs.addObserver(this, "perm-changed");
 | 
						|
      this._isObserving = true;
 | 
						|
    }
 | 
						|
 | 
						|
    this._type = params.permissionType;
 | 
						|
    this._list = document.getElementById("permissionsBox");
 | 
						|
    this._removeButton = document.getElementById("removePermission");
 | 
						|
    this._removeAllButton = document.getElementById("removeAllPermissions");
 | 
						|
    this._searchBox = document.getElementById("searchBox");
 | 
						|
    this._checkbox = document.getElementById("permissionsDisableCheckbox");
 | 
						|
    this._disableExtensionButton = document.getElementById("disableNotificationsPermissionExtension");
 | 
						|
    this._permissionsDisableDescription = document.getElementById("permissionsDisableDescription");
 | 
						|
 | 
						|
    let permissionsText = document.getElementById("permissionsText");
 | 
						|
 | 
						|
    let l10n = sitePermissionsL10n[this._type];
 | 
						|
    document.l10n.setAttributes(permissionsText, l10n.description);
 | 
						|
    document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
 | 
						|
    document.l10n.setAttributes(this._permissionsDisableDescription, l10n.disableDescription);
 | 
						|
    document.l10n.setAttributes(document.documentElement, l10n.window);
 | 
						|
 | 
						|
    await document.l10n.translateElements([
 | 
						|
      permissionsText,
 | 
						|
      this._checkbox,
 | 
						|
      this._permissionsDisableDescription,
 | 
						|
      document.documentElement,
 | 
						|
    ]);
 | 
						|
 | 
						|
    // Initialize the checkbox state and handle showing notification permission UI
 | 
						|
    // when it is disabled by an extension.
 | 
						|
    this._defaultPermissionStatePrefName = "permissions.default." + this._type;
 | 
						|
    this._watchPermissionPrefChange();
 | 
						|
 | 
						|
    this._loadPermissions();
 | 
						|
    this.buildPermissionsList();
 | 
						|
 | 
						|
    this._searchBox.focus();
 | 
						|
  },
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    if (this._isObserving) {
 | 
						|
      Services.obs.removeObserver(this, "perm-changed");
 | 
						|
      this._isObserving = false;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  observe(subject, topic, data) {
 | 
						|
    if (topic !== "perm-changed")
 | 
						|
      return;
 | 
						|
 | 
						|
    let permission = subject.QueryInterface(Ci.nsIPermission);
 | 
						|
 | 
						|
    // Ignore unrelated permission types and permissions with unknown states.
 | 
						|
    if (permission.type !== this._type || !PERMISSION_STATES.includes(permission.capability))
 | 
						|
      return;
 | 
						|
 | 
						|
    if (data == "added") {
 | 
						|
      this._addPermissionToList(permission);
 | 
						|
      this.buildPermissionsList();
 | 
						|
    } else if (data == "changed") {
 | 
						|
      let p = this._permissions.get(permission.principal.origin);
 | 
						|
      p.capability = permission.capability;
 | 
						|
      p.l10nId = this._getCapabilityString(permission.capability);
 | 
						|
      this._handleCapabilityChange(p);
 | 
						|
      this.buildPermissionsList();
 | 
						|
    } else if (data == "deleted") {
 | 
						|
      this._removePermissionFromList(permission.principal.origin);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _handleCapabilityChange(perm) {
 | 
						|
    let permissionlistitem = document.getElementsByAttribute("origin", perm.origin)[0];
 | 
						|
    let menulist = permissionlistitem.getElementsByTagName("menulist")[0];
 | 
						|
    menulist.selectedItem =
 | 
						|
      menulist.getElementsByAttribute("value", perm.capability)[0];
 | 
						|
  },
 | 
						|
 | 
						|
  _handleCheckboxUIUpdates() {
 | 
						|
    let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
 | 
						|
    if (pref != Services.prefs.PREF_INVALID) {
 | 
						|
      this._currentDefaultPermissionsState = Services.prefs.getIntPref(this._defaultPermissionStatePrefName);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._currentDefaultPermissionsState === null) {
 | 
						|
      this._checkbox.setAttribute("hidden", true);
 | 
						|
      this._permissionsDisableDescription.setAttribute("hidden", true);
 | 
						|
    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
 | 
						|
      this._checkbox.checked = true;
 | 
						|
    } else {
 | 
						|
      this._checkbox.checked = false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
 | 
						|
      this._checkbox.disabled = true;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
  * Listen for changes to the permissions.default.* pref and make
 | 
						|
  * necessary changes to the UI.
 | 
						|
  */
 | 
						|
  _watchPermissionPrefChange() {
 | 
						|
    this._handleCheckboxUIUpdates();
 | 
						|
 | 
						|
    if (this._type == "desktop-notification") {
 | 
						|
      this._handleWebNotificationsDisable();
 | 
						|
 | 
						|
      this._disableExtensionButton.addEventListener(
 | 
						|
        "command",
 | 
						|
        makeDisableControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY)
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    let observer = () => {
 | 
						|
      this._handleCheckboxUIUpdates();
 | 
						|
      if (this._type == "desktop-notification") {
 | 
						|
        this._handleWebNotificationsDisable();
 | 
						|
      }
 | 
						|
    };
 | 
						|
    Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer);
 | 
						|
    window.addEventListener("unload", () => {
 | 
						|
      Services.prefs.removeObserver(this._defaultPermissionStatePrefName, observer);
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
  * Handles the UI update for web notifications disable by extensions.
 | 
						|
  */
 | 
						|
  async _handleWebNotificationsDisable() {
 | 
						|
    let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF);
 | 
						|
    if (prefLocked) {
 | 
						|
      // An extension can't control these settings if they're locked.
 | 
						|
      hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
 | 
						|
    } else {
 | 
						|
      let isControlled = await handleControllingExtension(PREF_SETTING_TYPE, NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
 | 
						|
      this._checkbox.disabled = isControlled;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _getCapabilityString(capability) {
 | 
						|
    let stringKey = null;
 | 
						|
    switch (capability) {
 | 
						|
    case Services.perms.ALLOW_ACTION:
 | 
						|
      stringKey = "permissions-capabilities-allow";
 | 
						|
      break;
 | 
						|
    case Services.perms.DENY_ACTION:
 | 
						|
      stringKey = "permissions-capabilities-block";
 | 
						|
      break;
 | 
						|
    case Services.perms.PROMPT_ACTION:
 | 
						|
      stringKey = "permissions-capabilities-prompt";
 | 
						|
      break;
 | 
						|
    default:
 | 
						|
      throw new Error(`Unknown capability: ${capability}`);
 | 
						|
    }
 | 
						|
    return stringKey;
 | 
						|
  },
 | 
						|
 | 
						|
  _addPermissionToList(perm) {
 | 
						|
    // Ignore unrelated permission types and permissions with unknown states.
 | 
						|
    if (perm.type !== this._type || !PERMISSION_STATES.includes(perm.capability))
 | 
						|
      return;
 | 
						|
    let l10nId = this._getCapabilityString(perm.capability);
 | 
						|
    let p = new Permission(perm.principal, perm.type, perm.capability, l10nId);
 | 
						|
    this._permissions.set(p.origin, p);
 | 
						|
  },
 | 
						|
 | 
						|
  _removePermissionFromList(origin) {
 | 
						|
    this._permissions.delete(origin);
 | 
						|
    let permissionlistitem = document.getElementsByAttribute("origin", origin)[0];
 | 
						|
    if (permissionlistitem) {
 | 
						|
      permissionlistitem.remove();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _loadPermissions() {
 | 
						|
    // load permissions into a table.
 | 
						|
    for (let nextPermission of Services.perms.enumerator) {
 | 
						|
      this._addPermissionToList(nextPermission);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _createPermissionListItem(permission) {
 | 
						|
    let richlistitem = document.createXULElement("richlistitem");
 | 
						|
    richlistitem.setAttribute("origin", permission.origin);
 | 
						|
    let row = document.createXULElement("hbox");
 | 
						|
    row.setAttribute("flex", "1");
 | 
						|
 | 
						|
    let hbox = document.createXULElement("hbox");
 | 
						|
    let website = document.createXULElement("label");
 | 
						|
    website.setAttribute("value", permission.origin);
 | 
						|
    website.setAttribute("width", "50");
 | 
						|
    hbox.setAttribute("class", "website-name");
 | 
						|
    hbox.setAttribute("flex", "3");
 | 
						|
    hbox.appendChild(website);
 | 
						|
 | 
						|
    let menulist = document.createXULElement("menulist");
 | 
						|
    let menupopup = document.createXULElement("menupopup");
 | 
						|
    menulist.setAttribute("flex", "1");
 | 
						|
    menulist.setAttribute("width", "50");
 | 
						|
    menulist.setAttribute("class", "website-status");
 | 
						|
    menulist.appendChild(menupopup);
 | 
						|
    let states = SitePermissions.getAvailableStates(permission.type);
 | 
						|
    for (let state of states) {
 | 
						|
      // Work around the (rare) edge case when a user has changed their
 | 
						|
      // default permission type back to UNKNOWN while still having a
 | 
						|
      // PROMPT permission set for an origin.
 | 
						|
      if (state == SitePermissions.UNKNOWN &&
 | 
						|
          permission.capability == SitePermissions.PROMPT) {
 | 
						|
        state = SitePermissions.PROMPT;
 | 
						|
      } else if (state == SitePermissions.UNKNOWN) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      let m = document.createXULElement("menuitem");
 | 
						|
      document.l10n.setAttributes(m, this._getCapabilityString(state));
 | 
						|
      m.setAttribute("value", state);
 | 
						|
      menupopup.appendChild(m);
 | 
						|
    }
 | 
						|
    menulist.value = permission.capability;
 | 
						|
 | 
						|
    menulist.addEventListener("select", () => {
 | 
						|
      this.onPermissionChange(permission, Number(menulist.selectedItem.value));
 | 
						|
    });
 | 
						|
 | 
						|
    row.appendChild(hbox);
 | 
						|
    row.appendChild(menulist);
 | 
						|
    richlistitem.appendChild(row);
 | 
						|
    return richlistitem;
 | 
						|
  },
 | 
						|
 | 
						|
  onWindowKeyPress(event) {
 | 
						|
    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE)
 | 
						|
      window.close();
 | 
						|
  },
 | 
						|
 | 
						|
  onPermissionKeyPress(event) {
 | 
						|
    if (!this._list.selectedItem)
 | 
						|
      return;
 | 
						|
 | 
						|
    if (event.keyCode == KeyEvent.DOM_VK_DELETE ||
 | 
						|
       (AppConstants.platform == "macosx" &&
 | 
						|
        event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)) {
 | 
						|
      this.onPermissionDelete();
 | 
						|
      event.preventDefault();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _setRemoveButtonState() {
 | 
						|
    if (!this._list)
 | 
						|
      return;
 | 
						|
 | 
						|
    let hasSelection = this._list.selectedIndex >= 0;
 | 
						|
    let hasRows = this._list.itemCount > 0;
 | 
						|
    this._removeButton.disabled = !hasSelection;
 | 
						|
    this._removeAllButton.disabled = !hasRows;
 | 
						|
  },
 | 
						|
 | 
						|
  onPermissionDelete() {
 | 
						|
    let richlistitem = this._list.selectedItem;
 | 
						|
    let origin = richlistitem.getAttribute("origin");
 | 
						|
    let permission = this._permissions.get(origin);
 | 
						|
 | 
						|
    this._removePermissionFromList(origin);
 | 
						|
    this._permissionsToDelete.set(permission.origin, permission);
 | 
						|
 | 
						|
    this._setRemoveButtonState();
 | 
						|
  },
 | 
						|
 | 
						|
  onAllPermissionsDelete() {
 | 
						|
    for (let permission of this._permissions.values()) {
 | 
						|
      this._removePermissionFromList(permission.origin);
 | 
						|
      this._permissionsToDelete.set(permission.origin, permission);
 | 
						|
    }
 | 
						|
 | 
						|
    this._setRemoveButtonState();
 | 
						|
  },
 | 
						|
 | 
						|
  onPermissionSelect() {
 | 
						|
    this._setRemoveButtonState();
 | 
						|
 | 
						|
    // If any item is selected, it should be the only item tabable
 | 
						|
    // in the richlistbox for accessibility reasons.
 | 
						|
    this._list.children.forEach((item) => {
 | 
						|
      let menulist = item.getElementsByTagName("menulist")[0];
 | 
						|
      if (!item.selected) {
 | 
						|
        menulist.setAttribute("tabindex", -1);
 | 
						|
      } else {
 | 
						|
        menulist.removeAttribute("tabindex");
 | 
						|
      }
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  onPermissionChange(perm, capability) {
 | 
						|
    let p = this._permissions.get(perm.origin);
 | 
						|
    if (p.capability == capability)
 | 
						|
      return;
 | 
						|
    p.capability = capability;
 | 
						|
    p.l10nId = this._getCapabilityString(capability);
 | 
						|
    this._permissionsToChange.set(p.origin, p);
 | 
						|
 | 
						|
    // enable "remove all" button as needed
 | 
						|
    this._setRemoveButtonState();
 | 
						|
  },
 | 
						|
 | 
						|
  onApplyChanges() {
 | 
						|
    // Stop observing permission changes since we are about
 | 
						|
    // to write out the pending adds/deletes and don't need
 | 
						|
    // to update the UI
 | 
						|
    this.uninit();
 | 
						|
 | 
						|
    for (let p of this._permissionsToChange.values()) {
 | 
						|
      let uri = Services.io.newURI(p.origin);
 | 
						|
      SitePermissions.set(uri, p.type, p.capability);
 | 
						|
    }
 | 
						|
 | 
						|
    for (let p of this._permissionsToDelete.values()) {
 | 
						|
      let uri = Services.io.newURI(p.origin);
 | 
						|
      SitePermissions.remove(uri, p.type);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this._checkbox.checked) {
 | 
						|
      Services.prefs.setIntPref(this._defaultPermissionStatePrefName, SitePermissions.BLOCK);
 | 
						|
    } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
 | 
						|
      Services.prefs.setIntPref(this._defaultPermissionStatePrefName, SitePermissions.UNKNOWN);
 | 
						|
    }
 | 
						|
 | 
						|
    window.close();
 | 
						|
  },
 | 
						|
 | 
						|
  buildPermissionsList(sortCol) {
 | 
						|
    // Clear old entries.
 | 
						|
    let oldItems = this._list.querySelectorAll("richlistitem");
 | 
						|
    for (let item of oldItems) {
 | 
						|
      item.remove();
 | 
						|
    }
 | 
						|
    let frag = document.createDocumentFragment();
 | 
						|
 | 
						|
    let permissions = Array.from(this._permissions.values());
 | 
						|
 | 
						|
    let keyword = this._searchBox.value.toLowerCase().trim();
 | 
						|
    for (let permission of permissions) {
 | 
						|
      if (keyword && !permission.origin.includes(keyword)) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      let richlistitem = this._createPermissionListItem(permission);
 | 
						|
      frag.appendChild(richlistitem);
 | 
						|
    }
 | 
						|
 | 
						|
    // Sort permissions.
 | 
						|
    this._sortPermissions(this._list, frag, sortCol);
 | 
						|
 | 
						|
    this._list.appendChild(frag);
 | 
						|
 | 
						|
    this._setRemoveButtonState();
 | 
						|
  },
 | 
						|
 | 
						|
  _sortPermissions(list, frag, column) {
 | 
						|
    let sortDirection;
 | 
						|
 | 
						|
    if (!column) {
 | 
						|
      column = document.querySelector("treecol[data-isCurrentSortCol=true]");
 | 
						|
      sortDirection = column.getAttribute("data-last-sortDirection") || "ascending";
 | 
						|
    } else {
 | 
						|
      sortDirection = column.getAttribute("data-last-sortDirection");
 | 
						|
      sortDirection = sortDirection === "ascending" ? "descending" : "ascending";
 | 
						|
    }
 | 
						|
 | 
						|
    let sortFunc = null;
 | 
						|
    switch (column.id) {
 | 
						|
      case "siteCol":
 | 
						|
        sortFunc = (a, b) => {
 | 
						|
          return comp.compare(a.getAttribute("origin"), b.getAttribute("origin"));
 | 
						|
        };
 | 
						|
        break;
 | 
						|
 | 
						|
      case "statusCol":
 | 
						|
        sortFunc = (a, b) => {
 | 
						|
          return parseInt(a.querySelector("menulist").value) >
 | 
						|
            parseInt(b.querySelector("menulist").value);
 | 
						|
        };
 | 
						|
        break;
 | 
						|
    }
 | 
						|
 | 
						|
    let comp = new Services.intl.Collator(undefined, {
 | 
						|
      usage: "sort",
 | 
						|
    });
 | 
						|
 | 
						|
    let items = Array.from(frag.querySelectorAll("richlistitem"));
 | 
						|
 | 
						|
    if (sortDirection === "descending") {
 | 
						|
      items.sort((a, b) => sortFunc(b, a));
 | 
						|
    } else {
 | 
						|
      items.sort(sortFunc);
 | 
						|
    }
 | 
						|
 | 
						|
    // Re-append items in the correct order:
 | 
						|
    items.forEach(item => frag.appendChild(item));
 | 
						|
 | 
						|
    let cols = list.querySelectorAll("treecol");
 | 
						|
    cols.forEach(c => {
 | 
						|
      c.removeAttribute("data-isCurrentSortCol");
 | 
						|
      c.removeAttribute("sortDirection");
 | 
						|
    });
 | 
						|
    column.setAttribute("data-isCurrentSortCol", "true");
 | 
						|
    column.setAttribute("sortDirection", sortDirection);
 | 
						|
    column.setAttribute("data-last-sortDirection", sortDirection);
 | 
						|
  },
 | 
						|
};
 |