tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 3e3e1240b3211f43c916947b5c8838ea183b8aa0
parent cf3d3685c671b7975061ee051b6cf37053ace852
Author: James Teh <jteh@mozilla.com>
Date:   Mon,  3 Nov 2025 22:30:52 +0000

Bug 1635774 part 2: Functionality to support customisation of keyboard shortcuts. r=mossop

This adds a CustomKeys module which provides the functionality to customise keyboard shortcuts defined by <key> elements.
Customisations are saved to and loaded from a JSON configuration file.
This is hooked into browser-init so that it can apply to all browser windows.

Differential Revision: https://phabricator.services.mozilla.com/D266791

Diffstat:
Mbrowser/base/content/browser-init.js | 6++++++
Abrowser/components/customkeys/CustomKeys.sys.mjs | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/moz.build | 14++++++++++++++
Abrowser/components/customkeys/tests/browser/browser.toml | 4++++
Abrowser/components/customkeys/tests/browser/browser_CustomKeys.js | 366+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/customkeys/tests/browser/head.js | 25+++++++++++++++++++++++++
Mbrowser/components/moz.build | 1+
7 files changed, 689 insertions(+), 0 deletions(-)

diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js @@ -3,6 +3,10 @@ * 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/. */ +const { CustomKeys } = ChromeUtils.importESModule( + "resource:///modules/CustomKeys.sys.mjs" +); + let _resolveDelayedStartup; var delayedStartupPromise = new Promise(resolve => { _resolveDelayedStartup = resolve; @@ -268,6 +272,7 @@ var gBrowserInit = { if (gToolbarKeyNavEnabled) { ToolbarKeyboardNavigator.init(); } + CustomKeys.initWindow(window); // Update UI if browser is under remote control. gRemoteControl.updateVisualCue(); @@ -1083,6 +1088,7 @@ var gBrowserInit = { if (gToolbarKeyNavEnabled) { ToolbarKeyboardNavigator.uninit(); } + CustomKeys.uninitWindow(window); // Bug 1952900 to allow switching to unload category without leaking ChromeUtils.importESModule( diff --git a/browser/components/customkeys/CustomKeys.sys.mjs b/browser/components/customkeys/CustomKeys.sys.mjs @@ -0,0 +1,273 @@ +/* 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 { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs"; + +// All the attributes for a <key> element that might specify the key to press. +// We include data-l10n-id because keys are often specified using Fluent. Note +// that we deliberately remove data-l10n-id when a key is customized because +// otherwise, Fluent would overwrite the customization. +const ATTRS = ["data-l10n-id", "key", "keycode", "modifiers"]; + +// The original keys for any shortcuts that have been customized. This is used +// to identify whether a shortcut has been customized, as well as to reset a +// shortcut. +const original = {}; +// All the browser windows we are handling. Maps from a window to our +// MutationObserver for that window. +const windows = new Map(); + +// The configuration is an Object with the form: +// { "someKeyId": { +// modifiers: "mod1,mod2", +// key: "someKey", +// keycode: "someKeycode" +// } } +// Only one of `key` or `keycode` should be specified for each entry. An entry +// may be empty to indicate that the default key has been cleared; i.e. +// { "someKeyId": {} } +const config = new JSONFile({ + path: PathUtils.join(PathUtils.profileDir, "customKeys.json"), +}); + +function applyToKeyEl(window, keyId, keysets) { + const keyEl = window.document.getElementById(keyId); + if (!keyEl || keyEl.tagName != "key") { + return; + } + if (!original[keyId]) { + // Save the original key in case the user wants to reset. + const orig = (original[keyId] = {}); + for (const attr of ATTRS) { + const val = keyEl.getAttribute(attr); + if (val) { + orig[attr] = val; + } + } + } + for (const attr of ATTRS) { + keyEl.removeAttribute(attr); + } + const data = config.data[keyId]; + for (const attr of ["modifiers", "key", "keycode"]) { + const val = data[attr]; + if (val) { + keyEl.setAttribute(attr, val); + } + } + keysets.add(keyEl.parentElement); +} + +function resetKeyEl(window, keyId, keysets) { + const keyEl = window.document.getElementById(keyId); + if (!keyEl) { + return; + } + const orig = original[keyId]; + for (const attr of ATTRS) { + keyEl.removeAttribute(attr); + const val = orig[attr]; + if (val !== undefined) { + keyEl.setAttribute(attr, val); + } + } + keysets.add(keyEl.parentElement); +} + +async function applyToNewWindow(window) { + await config.load(); + const keysets = new Set(); + for (const keyId in config.data) { + applyToKeyEl(window, keyId, keysets); + } + refreshKeysets(window, keysets); + observe(window); +} + +function refreshKeysets(window, keysets) { + if (keysets.size == 0) { + return; + } + const observer = windows.get(window); + if (observer) { + // We don't want our MutationObserver to process this. + observer.disconnect(); + } + // Gecko doesn't watch for changes to key elements. It only sets up a key + // element when its keyset is bound to the tree. See: + // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587 + // Therefore, remove and re-add any modified keysets to apply the changes. + for (const keyset of keysets) { + const parent = keyset.parentElement; + keyset.remove(); + parent.append(keyset); + } + if (observer) { + observe(window); + } +} + +function observe(window) { + let observer = windows.get(window); + if (!observer) { + // A keyset can be added dynamically. For example, DevTools does this during + // delayed startup. Note that key elements cannot be added dynamically. We + // know this because Gecko doesn't watch for changes to key elements. It + // only sets up a key element when its keyset is bound to the tree. See: + // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587 + observer = new window.MutationObserver(mutations => { + const keysets = new Set(); + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.tagName != "keyset") { + continue; + } + for (const key of node.children) { + if (key.tagName != "key" || !config.data[key.id]) { + continue; + } + applyToKeyEl(window, key.id, keysets); + } + } + } + refreshKeysets(window, keysets); + }); + windows.set(window, observer); + } + for (const node of [window.document, window.document.body]) { + observer.observe(node, { childList: true }); + } + return observer; +} + +export const CustomKeys = { + /** + * Customize a keyboard shortcut. + * + * @param {string} id The id of the key element. + * @param {object} data + * @param {string} data.modifiers The modifiers for the key; e.g. "accel,shift". + * @param {string} data.key The key itself; e.g. "Y". Mutually exclusive with data.keycode. + * @param {string} data.keycode The key code; e.g. VK_F1. Mutually exclusive with data.key. + */ + changeKey(id, { modifiers, key, keycode }) { + const existing = config.data[id]; + if ( + existing && + modifiers == existing.modifiers && + key == existing.key && + keycode == existing.keycode + ) { + return; // No change. + } + const defaultKey = this.getDefaultKey(id); + if ( + defaultKey && + modifiers == defaultKey.modifiers && + key == defaultKey.key && + keycode == defaultKey.keycode + ) { + this.resetKey(id); + return; + } + const data = (config.data[id] = {}); + if (modifiers) { + data.modifiers = modifiers; + } + if (key) { + data.key = key; + } + if (keycode) { + data.keycode = keycode; + } + for (const window of windows.keys()) { + const keysets = new Set(); + applyToKeyEl(window, id, keysets); + refreshKeysets(window, keysets); + } + config.saveSoon(); + }, + + /** + * Reset a keyboard shortcut to its defalt. + * + * @param {string} id The id of the key element. + */ + resetKey(id) { + if (!config.data[id]) { + return; // No change. + } + delete config.data[id]; + for (const window of windows.keys()) { + const keysets = new Set(); + resetKeyEl(window, id, keysets); + refreshKeysets(window, keysets); + } + delete original[id]; + config.saveSoon(); + }, + + /** + * Clear a keyboard shortcut; i.e. so it does nothing. + * + * @param {string} id The id of the key element. + */ + clearKey(id) { + this.changeKey(id, {}); + }, + + /** + * Reset all keyboard shortcuts to their defaults. + */ + resetAll() { + config.data = {}; + for (const window of windows.keys()) { + const keysets = new Set(); + for (const id in original) { + resetKeyEl(window, id, keysets); + } + refreshKeysets(window, keysets); + } + for (const id of Object.keys(original)) { + delete original[id]; + } + config.saveSoon(); + }, + + initWindow(window) { + applyToNewWindow(window); + }, + + uninitWindow(window) { + windows.get(window).disconnect(); + windows.delete(window); + }, + + /** + * Return the default key for this shortcut in the form: + * { modifiers: "mod1,mod2", key: "someKey", keycode: "someKeycode" } + * If this shortcut isn't customized, return null. + * + * @param {string} keyId The id of the key element. + */ + getDefaultKey(keyId) { + const origKey = original[keyId]; + if (!origKey) { + return null; + } + const data = {}; + if (origKey.modifiers) { + data.modifiers = origKey.modifiers; + } + if (origKey.key) { + data.key = origKey.key; + } + if (origKey.keycode) { + data.keycode = origKey.keycode; + } + return data; + }, +}; + +Object.freeze(CustomKeys); diff --git a/browser/components/customkeys/moz.build b/browser/components/customkeys/moz.build @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES += [ + "CustomKeys.sys.mjs", +] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Keyboard Navigation") diff --git a/browser/components/customkeys/tests/browser/browser.toml b/browser/components/customkeys/tests/browser/browser.toml @@ -0,0 +1,4 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_CustomKeys.js"] diff --git a/browser/components/customkeys/tests/browser/browser_CustomKeys.js b/browser/components/customkeys/tests/browser/browser_CustomKeys.js @@ -0,0 +1,366 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the behavior of assigning keys, resetting keys, etc. via the CustomKeys module. The UI is tested separately. + */ + +registerCleanupFunction(() => { + CustomKeys.resetAll(); +}); + +// Test changing a key. +add_task(async function testChangeKey() { + is( + document.activeElement, + gBrowser.selectedBrowser, + "Tab document browser is focused" + ); + + is( + CustomKeys.getDefaultKey("key_gotoHistory"), + null, + "key_gotoHistory is not customized" + ); + info(`Changing key_gotoHistory to ${consts.unusedDisplay}`); + CustomKeys.changeKey("key_gotoHistory", { + modifiers: consts.unusedModifiers, + key: consts.unusedKey, + }); + Assert.deepEqual( + CustomKeys.getDefaultKey("key_gotoHistory"), + { modifiers: consts.historyModifiers, key: "H" }, + "key_gotoHistory is customized" + ); + info(`Pressing ${consts.unusedDisplay}`); + let focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused"); + EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, window); + await focused; + ok(true, "Sidebar got focus"); + is( + SidebarController.currentID, + "viewHistorySidebar", + "History sidebar is open" + ); + info(`Pressing ${consts.unusedDisplay}`); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, window); + await focused; + ok(true, "Tab document browser got focus"); + + is( + CustomKeys.getDefaultKey("viewBookmarksSidebarKb"), + null, + "viewBookmarksSidebarKb is not customized" + ); + info(`Changing viewBookmarksSidebarKb to ${consts.historyDisplay}`); + CustomKeys.changeKey("viewBookmarksSidebarKb", { + modifiers: consts.historyModifiers, + key: "H", + }); + Assert.deepEqual( + CustomKeys.getDefaultKey("viewBookmarksSidebarKb"), + { modifiers: "accel", key: "B" }, + "viewBookmarksSidebarKb is customized" + ); + info(`Pressing ${consts.historyDisplay}`); + focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused"); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await focused; + ok(true, "Sidebar got focus"); + is( + SidebarController.currentID, + "viewBookmarksSidebar", + "Bookmarks sidebar is open" + ); + info(`Pressing ${consts.historyDisplay}`); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await focused; + ok(true, "Tab document browser got focus"); + + info("Resetting all keys"); + CustomKeys.resetAll(); +}); + +// Test clearing a key. +add_task(async function testClearKey() { + is( + document.activeElement, + gBrowser.selectedBrowser, + "Tab document browser is focused" + ); + // Move focus into chrome so that accel+l to focus the URL bar reliably occurs + // immediately. We need this guarantee because we want to test when pressing + // accel+l does nothing, so we can't rely on an event for that test. + info("Focusing selected tab"); + let focused = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "focus"); + gBrowser.selectedTab.focus(); + await focused; + ok(true, "Selected tab got focus"); + + is( + CustomKeys.getDefaultKey("focusURLBar"), + null, + "focusURLBar is not customized" + ); + info("Pressing accel+L"); + EventUtils.synthesizeKey("L", { accelKey: true }, window); + is(document.activeElement, gURLBar.inputField, "URL bar is focused"); + info("Focusing selected tab"); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedTab, "focus"); + gBrowser.selectedTab.focus(); + await focused; + ok(true, "Selected tab got focus"); + + info("Clearing focusURLBar"); + CustomKeys.clearKey("focusURLBar"); + Assert.deepEqual( + CustomKeys.getDefaultKey("focusURLBar"), + { modifiers: "accel", key: "L" }, + "focusURLBar is customized" + ); + info("Pressing accel+L"); + EventUtils.synthesizeKey("L", { accelKey: true }, window); + is( + document.activeElement, + gBrowser.selectedTab, + "Selected tab still focused" + ); + + // The tab bar has focus now. We need to move focus back to the document + // because otherwise, the focus will remain on the tab bar for the next test. + info("Focusing tab document browser"); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + gBrowser.selectedBrowser.focus(); + await focused; + ok(true, "Tab document browser got focus"); + info("Resetting all keys"); + CustomKeys.resetAll(); +}); + +// Test resetting a key. +add_task(async function testResetKey() { + is( + document.activeElement, + gBrowser.selectedBrowser, + "Tab document browser is focused" + ); + + is( + CustomKeys.getDefaultKey("focusURLBar"), + null, + "focusURLBar is not customized" + ); + info("Clearing focusURLBar"); + CustomKeys.clearKey("focusURLBar"); + Assert.deepEqual( + CustomKeys.getDefaultKey("focusURLBar"), + { modifiers: "accel", key: "L" }, + "focusURLBar is customized" + ); + + info("Resetting focusURLBar"); + CustomKeys.resetKey("focusURLBar"); + is( + CustomKeys.getDefaultKey("focusURLBar"), + null, + "focusURLBar is not customized" + ); + info("Pressing accel+L"); + let focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus"); + EventUtils.synthesizeKey("L", { accelKey: true }, window); + await focused; + ok(true, "URL bar got focus"); + info("Focusing tab document browser"); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + gBrowser.selectedBrowser.focus(); + await focused; + ok(true, "Tab document browser got focus"); + + info("Resetting all keys"); + CustomKeys.resetAll(); +}); + +// Test resetting all keys. +add_task(async function testResetAll() { + is( + document.activeElement, + gBrowser.selectedBrowser, + "Tab document browser is focused" + ); + + is( + CustomKeys.getDefaultKey("key_gotoHistory"), + null, + "key_gotoHistory is not customized" + ); + info(`Changing key_gotoHistory to ${consts.unusedDisplay}`); + CustomKeys.changeKey("key_gotoHistory", { + modifiers: consts.unusedModifiers, + key: consts.unusedKey, + }); + Assert.deepEqual( + CustomKeys.getDefaultKey("key_gotoHistory"), + { modifiers: consts.historyModifiers, key: "H" }, + "key_gotoHistory is customized" + ); + + is( + CustomKeys.getDefaultKey("focusURLBar"), + null, + "focusURLBar is not customized" + ); + info("Clearing focusURLBar"); + CustomKeys.clearKey("focusURLBar"); + Assert.deepEqual( + CustomKeys.getDefaultKey("focusURLBar"), + { modifiers: "accel", key: "L" }, + "focusURLBar is customized" + ); + + info("Resetting all keys"); + CustomKeys.resetAll(); + + info(`Pressing ${consts.historyDisplay}`); + let focused = BrowserTestUtils.waitForEvent(window, "SidebarFocused"); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await focused; + ok(true, "Sidebar got focus"); + is( + SidebarController.currentID, + "viewHistorySidebar", + "History sidebar is open" + ); + info(`Pressing ${consts.historyDisplay}`); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + EventUtils.synthesizeKey("H", consts.historyOptions, window); + await focused; + ok(true, "Tab document browser got focus"); + + info("Pressing accel+L"); + focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus"); + EventUtils.synthesizeKey("L", { accelKey: true }, window); + await focused; + ok(true, "URL bar got focus"); + info("Focusing tab document browser"); + focused = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "focus"); + gBrowser.selectedBrowser.focus(); + await focused; + ok(true, "Tab document browser got focus"); +}); + +// Test that changes apply to other windows. +add_task(async function testOtherWindow() { + // Test changing a key before a new window is opened. + is( + CustomKeys.getDefaultKey("key_gotoHistory"), + null, + "key_gotoHistory is not customized" + ); + info(`Changing key_gotoHistory to ${consts.unusedDisplay}`); + CustomKeys.changeKey("key_gotoHistory", { + modifiers: consts.unusedModifiers, + key: consts.unusedKey, + }); + Assert.deepEqual( + CustomKeys.getDefaultKey("key_gotoHistory"), + { modifiers: consts.historyModifiers, key: "H" }, + "key_gotoHistory is customized" + ); + + info("Opening new window"); + const newWin = await BrowserTestUtils.openNewBrowserWindow(); + is( + newWin.document.activeElement, + newWin.gURLBar.inputField, + "URL bar is focused in new window" + ); + + info(`Pressing ${consts.unusedDisplay} in new window`); + let focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused"); + EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, newWin); + await focused; + ok(true, "Sidebar got focus in new window"); + is( + newWin.SidebarController.currentID, + "viewHistorySidebar", + "History sidebar is open in new window" + ); + info(`Pressing ${consts.unusedDisplay} in new window`); + focused = BrowserTestUtils.waitForEvent( + newWin.gBrowser.selectedBrowser, + "focus" + ); + EventUtils.synthesizeKey(consts.unusedKey, consts.unusedOptions, newWin); + await focused; + ok(true, "Tab document browser got focus in new window"); + + // Test changing a key after a new window is opened. + is( + CustomKeys.getDefaultKey("viewBookmarksSidebarKb"), + null, + "viewBookmarksSidebarKb is not customized" + ); + info(`Changing viewBookmarksSidebarKb to ${consts.historyDisplay}`); + CustomKeys.changeKey("viewBookmarksSidebarKb", { + modifiers: consts.historyModifiers, + key: "H", + }); + Assert.deepEqual( + CustomKeys.getDefaultKey("viewBookmarksSidebarKb"), + { modifiers: "accel", key: "B" }, + "viewBookmarksSidebarKb is customized" + ); + info(`Pressing ${consts.historyDisplay} in new window`); + focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused"); + EventUtils.synthesizeKey("H", consts.historyOptions, newWin); + await focused; + ok(true, "Sidebar got focus in new window"); + is( + newWin.SidebarController.currentID, + "viewBookmarksSidebar", + "Bookmarks sidebar is open" + ); + info(`Pressing ${consts.historyDisplay} in new window`); + focused = BrowserTestUtils.waitForEvent( + newWin.gBrowser.selectedBrowser, + "focus" + ); + EventUtils.synthesizeKey("H", consts.historyOptions, newWin); + await focused; + ok(true, "Tab document browser got focus in new window"); + + // Test resetting keys after a new window is opened. + info("Resetting all keys"); + CustomKeys.resetAll(); + + is( + CustomKeys.getDefaultKey("key_gotoHistory"), + null, + "key_gotoHistory is not customized" + ); + info(`Pressing ${consts.historyDisplay} in new window`); + focused = BrowserTestUtils.waitForEvent(newWin, "SidebarFocused"); + EventUtils.synthesizeKey("H", consts.historyOptions, newWin); + await focused; + ok(true, "Sidebar got focus in new window"); + is( + newWin.SidebarController.currentID, + "viewHistorySidebar", + "History sidebar is open in new window" + ); + info(`Pressing ${consts.historyDisplay} in new window`); + focused = BrowserTestUtils.waitForEvent( + newWin.gBrowser.selectedBrowser, + "focus" + ); + EventUtils.synthesizeKey("H", consts.historyOptions, newWin); + await focused; + ok(true, "Tab document browser got focus in new window"); + + info("Closing new window"); + await BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/components/customkeys/tests/browser/head.js b/browser/components/customkeys/tests/browser/head.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const isMac = AppConstants.platform == "macosx"; +const isLinux = AppConstants.platform == "linux"; + +const consts = { + // The following constants specify the default key combinations for various + // commands. They must be updated if these change in future. + // key_gotoHistory + historyDisplay: isMac ? "⇧⌘H" : "Ctrl+H", + historyModifiers: isMac ? "accel,shift" : "accel", + historyOptions: { accelKey: true, shiftKey: isMac }, + + // The following unused* constants specify a key combination which is unused by + // default. This will need to be updated if this key combination is assigned to + // something by default in future. + unusedModifiers: "accel,shift", + unusedOptions: { accelKey: true, shiftKey: true }, + unusedKey: isLinux ? "Q" : "Y", + unusedModifiersDisplay: isMac ? "⇧⌘" : "Ctrl+Shift+", +}; +consts.unusedDisplay = `${consts.unusedModifiersDisplay}${consts.unusedKey}`; diff --git a/browser/components/moz.build b/browser/components/moz.build @@ -36,6 +36,7 @@ DIRS += [ "contentanalysis", "contextualidentity", "customizableui", + "customkeys", "downloads", "enterprisepolicies", "extensions",