Files
tubestation/browser/components/preferences/dialogs/sitePermissions.js
Gijs Kruitbosch 450ac5ac39 Bug 1794757 - autoplay dialog should wait for translation to finish before showing and determining size to avoid buttons being cut off, r=kpatenio
The sitePermissions code is reused, and all dialogs work OK except the autoplay one.
This is because the autoplay code adds extra content not present in the other
dialogs, and localizes it.
The SubDialog code relies on determining the size of the dialog once the
mozSubDialogReady promise resolves, which it does before fluent puts in l10n data
for the content that was just added. This patch fixes that.

It also, as a driveby, correctly pauses and resumes observing l10n attributes
in the doc when making modifications, to avoid fluent doing a second pass for
the same content.

Differential Revision: https://phabricator.services.mozilla.com/D159543
2022-10-19 14:59:22 +00:00

616 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();
// If any item is selected, it should be the only item tabable
// in the richlistbox for accessibility reasons.
this._list.itemChildren.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(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);
},
};