Rather than having each richlistbox consumer having to reinvent focus patterns for buttons and menulists in its 'rich' items, let's just teach richlistbox and richlistitem to not suck at keyboard navigation. That way we won't keep forgetting to deal with this whenever we add new lists anywhere. This allows us to remove the custom handling in sitePermissions.js, and the same handling should be covered by the existing test, ie browser/components/preferences/tests/browser_permissions_dialog.js To summarize the desired keyboard behaviour: - tab/shift-tab move focus to controls inside selected items only (not other rows) - arrow keys move the list selection up/down - when arrowing to move the list selection, focus moves with the selection if it was previously on a control in the previously selected item. Differential Revision: https://phabricator.services.mozilla.com/D161528
605 lines
18 KiB
JavaScript
605 lines
18 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 ../extensionControlled.js */
|
|
|
|
var { AppConstants } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AppConstants.sys.mjs"
|
|
);
|
|
const { SitePermissions } = ChromeUtils.import(
|
|
"resource:///modules/SitePermissions.jsm"
|
|
);
|
|
|
|
const sitePermissionsL10n = {
|
|
"desktop-notification": {
|
|
window: "permissions-site-notification-window2",
|
|
description: "permissions-site-notification-desc",
|
|
disableLabel: "permissions-site-notification-disable-label",
|
|
disableDescription: "permissions-site-notification-disable-desc",
|
|
},
|
|
geo: {
|
|
window: "permissions-site-location-window2",
|
|
description: "permissions-site-location-desc",
|
|
disableLabel: "permissions-site-location-disable-label",
|
|
disableDescription: "permissions-site-location-disable-desc",
|
|
},
|
|
xr: {
|
|
window: "permissions-site-xr-window2",
|
|
description: "permissions-site-xr-desc",
|
|
disableLabel: "permissions-site-xr-disable-label",
|
|
disableDescription: "permissions-site-xr-disable-desc",
|
|
},
|
|
camera: {
|
|
window: "permissions-site-camera-window2",
|
|
description: "permissions-site-camera-desc",
|
|
disableLabel: "permissions-site-camera-disable-label",
|
|
disableDescription: "permissions-site-camera-disable-desc",
|
|
},
|
|
microphone: {
|
|
window: "permissions-site-microphone-window2",
|
|
description: "permissions-site-microphone-desc",
|
|
disableLabel: "permissions-site-microphone-disable-label",
|
|
disableDescription: "permissions-site-microphone-disable-desc",
|
|
},
|
|
"autoplay-media": {
|
|
window: "permissions-site-autoplay-window2",
|
|
description: "permissions-site-autoplay-desc",
|
|
},
|
|
};
|
|
|
|
const sitePermissionsConfig = {
|
|
"autoplay-media": {
|
|
_getCapabilityString(capability) {
|
|
switch (capability) {
|
|
case SitePermissions.ALLOW:
|
|
return "permissions-capabilities-autoplay-allow";
|
|
case SitePermissions.BLOCK:
|
|
return "permissions-capabilities-autoplay-block";
|
|
case SitePermissions.AUTOPLAY_BLOCKED_ALL:
|
|
return "permissions-capabilities-autoplay-blockall";
|
|
}
|
|
throw new Error(`Unknown capability: ${capability}`);
|
|
},
|
|
},
|
|
};
|
|
|
|
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,
|
|
SitePermissions.AUTOPLAY_BLOCKED_ALL,
|
|
];
|
|
|
|
const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
|
|
const NOTIFICATIONS_PERMISSION_PREF =
|
|
"permissions.default.desktop-notification";
|
|
|
|
const AUTOPLAY_PREF = "media.autoplay.default";
|
|
|
|
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;
|
|
}
|
|
|
|
document.addEventListener("dialogaccept", () => this.onApplyChanges());
|
|
|
|
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"
|
|
);
|
|
this._setAutoplayPref = document.getElementById("setAutoplayPref");
|
|
|
|
let permissionsText = document.getElementById("permissionsText");
|
|
|
|
document.l10n.pauseObserving();
|
|
let l10n = sitePermissionsL10n[this._type];
|
|
document.l10n.setAttributes(permissionsText, l10n.description);
|
|
if (l10n.disableLabel) {
|
|
document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
|
|
}
|
|
if (l10n.disableDescription) {
|
|
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,
|
|
]);
|
|
document.l10n.resumeObserving();
|
|
|
|
// 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();
|
|
|
|
if (params.permissionType == "autoplay-media") {
|
|
await this.buildAutoplayMenulist();
|
|
this._setAutoplayPref.hidden = false;
|
|
}
|
|
|
|
this._searchBox.focus();
|
|
},
|
|
|
|
uninit() {
|
|
if (this._isObserving) {
|
|
Services.obs.removeObserver(this, "perm-changed");
|
|
this._isObserving = false;
|
|
}
|
|
if (this._setAutoplayPref) {
|
|
this._setAutoplayPref.hidden = true;
|
|
}
|
|
},
|
|
|
|
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.type,
|
|
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.hidden = true;
|
|
this._permissionsDisableDescription.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(type, capability) {
|
|
if (
|
|
type in sitePermissionsConfig &&
|
|
sitePermissionsConfig[type]._getCapabilityString
|
|
) {
|
|
return sitePermissionsConfig[type]._getCapabilityString(capability);
|
|
}
|
|
|
|
switch (capability) {
|
|
case Services.perms.ALLOW_ACTION:
|
|
return "permissions-capabilities-allow";
|
|
case Services.perms.DENY_ACTION:
|
|
return "permissions-capabilities-block";
|
|
case Services.perms.PROMPT_ACTION:
|
|
return "permissions-capabilities-prompt";
|
|
default:
|
|
throw new Error(`Unknown capability: ${capability}`);
|
|
}
|
|
},
|
|
|
|
_addPermissionToList(perm) {
|
|
// Ignore unrelated permission types and permissions with unknown states.
|
|
if (
|
|
perm.type !== this._type ||
|
|
!PERMISSION_STATES.includes(perm.capability) ||
|
|
// Skip private browsing session permissions
|
|
(perm.principal.privateBrowsingId !==
|
|
Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
|
|
perm.expireType === Services.perms.EXPIRE_SESSION)
|
|
) {
|
|
return;
|
|
}
|
|
let l10nId = this._getCapabilityString(perm.type, 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.all) {
|
|
this._addPermissionToList(nextPermission);
|
|
}
|
|
},
|
|
|
|
_createPermissionListItem(permission) {
|
|
let width = "75";
|
|
let richlistitem = document.createXULElement("richlistitem");
|
|
richlistitem.setAttribute("origin", permission.origin);
|
|
let row = document.createXULElement("hbox");
|
|
row.setAttribute("style", "-moz-box-flex: 1");
|
|
|
|
let hbox = document.createXULElement("hbox");
|
|
let website = document.createXULElement("label");
|
|
website.setAttribute("value", permission.origin);
|
|
website.setAttribute("width", width);
|
|
hbox.setAttribute("class", "website-name");
|
|
hbox.setAttribute("style", "-moz-box-flex: 3");
|
|
hbox.appendChild(website);
|
|
|
|
let menulist = document.createXULElement("menulist");
|
|
menulist.setAttribute("style", "-moz-box-flex: 1");
|
|
menulist.setAttribute("width", width);
|
|
menulist.setAttribute("class", "website-status");
|
|
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 = menulist.appendItem(undefined, state);
|
|
document.l10n.setAttributes(
|
|
m,
|
|
this._getCapabilityString(permission.type, state)
|
|
);
|
|
}
|
|
menulist.value = permission.capability;
|
|
|
|
menulist.addEventListener("select", () => {
|
|
this.onPermissionChange(permission, Number(menulist.value));
|
|
});
|
|
|
|
row.appendChild(hbox);
|
|
row.appendChild(menulist);
|
|
richlistitem.appendChild(row);
|
|
return richlistitem;
|
|
},
|
|
|
|
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();
|
|
},
|
|
|
|
onPermissionChange(perm, capability) {
|
|
let p = this._permissions.get(perm.origin);
|
|
if (p.capability == capability) {
|
|
return;
|
|
}
|
|
p.capability = capability;
|
|
p.l10nId = this._getCapabilityString(perm.type, perm.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()) {
|
|
SitePermissions.setForPrincipal(p.principal, p.type, p.capability);
|
|
}
|
|
|
|
for (let p of this._permissionsToDelete.values()) {
|
|
SitePermissions.removeFromPrincipal(p.principal, 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
|
|
);
|
|
}
|
|
},
|
|
|
|
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();
|
|
},
|
|
|
|
async buildAutoplayMenulist() {
|
|
let menulist = document.createXULElement("menulist");
|
|
let states = SitePermissions.getAvailableStates("autoplay-media");
|
|
document.l10n.pauseObserving();
|
|
for (let state of states) {
|
|
let m = menulist.appendItem(undefined, state);
|
|
document.l10n.setAttributes(
|
|
m,
|
|
this._getCapabilityString("autoplay-media", state)
|
|
);
|
|
}
|
|
|
|
menulist.value = SitePermissions.getDefault("autoplay-media");
|
|
|
|
menulist.addEventListener("select", () => {
|
|
SitePermissions.setDefault("autoplay-media", Number(menulist.value));
|
|
});
|
|
|
|
menulist.menupopup.setAttribute("incontentshell", "false");
|
|
|
|
menulist.disabled = Services.prefs.prefIsLocked(AUTOPLAY_PREF);
|
|
|
|
document.getElementById("setAutoplayPref").appendChild(menulist);
|
|
await document.l10n.translateFragment(menulist);
|
|
document.l10n.resumeObserving();
|
|
},
|
|
|
|
_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.previousElementSibling.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);
|
|
},
|
|
};
|