/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); const ACTION_ID_BOOKMARK = "bookmark"; const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator"; const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator"; const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions"; const PERSISTED_ACTIONS_CURRENT_VERSION = 1; // Escapes the given raw URL string, and returns an equivalent CSS url() // value for it. function escapeCSSURL(url) { return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`; } export var PageActions = { /** * Initializes PageActions. * * @param {boolean} addShutdownBlocker * This param exists only for tests. Normally the default value of true * must be used. */ init(addShutdownBlocker = true) { this._initBuiltInActions(); let callbacks = this._deferredAddActionCalls; delete this._deferredAddActionCalls; this._loadPersistedActions(); // Register the built-in actions, which are defined below in this file. for (let options of gBuiltInActions) { if (!this.actionForID(options.id)) { this._registerAction(new Action(options)); } } // Now place them all in each window. Instead of splitting the register and // place steps, we could simply call addAction, which does both, but doing // it this way means that all windows initially place their actions in the // urlbar the same way -- placeAllActions -- regardless of whether they're // open when this method is called or opened later. for (let bpa of allBrowserPageActions()) { bpa.placeAllActionsInUrlbar(); } // These callbacks are deferred until init happens and all built-in actions // are added. while (callbacks && callbacks.length) { callbacks.shift()(); } if (addShutdownBlocker) { // Purge removed actions from persisted state on shutdown. The point is // not to do it on Action.remove(). That way actions that are removed and // re-added while the app is running will have their urlbar placement and // other state remembered and restored. This happens for upgraded and // downgraded extensions, for example. lazy.AsyncShutdown.profileBeforeChange.addBlocker( "PageActions: purging unregistered actions from cache", () => this._purgeUnregisteredPersistedActions() ); } }, _deferredAddActionCalls: [], /** * A list of all Action objects, not in any particular order. Not live. * (array of Action objects) */ get actions() { let lists = [ this._builtInActions, this._nonBuiltInActions, this._transientActions, ]; return lists.reduce((memo, list) => memo.concat(list), []); }, /** * The list of Action objects that should appear in the panel for a given * window, sorted in the order in which they appear. If there are both * built-in and non-built-in actions, then the list will include the separator * between the two. The list is not live. (array of Action objects) * * @param browserWindow (DOM window, required) * This window's actions will be returned. * @return (array of PageAction.Action objects) The actions currently in the * given window's panel. */ actionsInPanel(browserWindow) { function filter(action) { return action.shouldShowInPanel(browserWindow); } let actions = this._builtInActions.filter(filter); let nonBuiltInActions = this._nonBuiltInActions.filter(filter); if (nonBuiltInActions.length) { if (actions.length) { actions.push( new Action({ id: ACTION_ID_BUILT_IN_SEPARATOR, _isSeparator: true, }) ); } actions.push(...nonBuiltInActions); } let transientActions = this._transientActions.filter(filter); if (transientActions.length) { if (actions.length) { actions.push( new Action({ id: ACTION_ID_TRANSIENT_SEPARATOR, _isSeparator: true, }) ); } actions.push(...transientActions); } return actions; }, /** * The list of actions currently in the urlbar, sorted in the order in which * they appear. Not live. * * @param browserWindow (DOM window, required) * This window's actions will be returned. * @return (array of PageAction.Action objects) The actions currently in the * given window's urlbar. */ actionsInUrlbar(browserWindow) { // Remember that IDs in idsInUrlbar may belong to actions that aren't // currently registered. return this._persistedActions.idsInUrlbar.reduce((actions, id) => { let action = this.actionForID(id); if (action && action.shouldShowInUrlbar(browserWindow)) { actions.push(action); } return actions; }, []); }, /** * Gets an action. * * @param id (string, required) * The ID of the action to get. * @return The Action object, or null if none. */ actionForID(id) { return this._actionsByID.get(id); }, /** * Registers an action. * * Actions are registered by their IDs. An error is thrown if an action with * the given ID has already been added. Use actionForID() before calling this * method if necessary. * * Be sure to call remove() on the action if the lifetime of the code that * owns it is shorter than the browser's -- if it lives in an extension, for * example. * * @param action (Action, required) * The Action object to register. * @return The given Action. */ addAction(action) { if (this._deferredAddActionCalls) { // init() hasn't been called yet. Defer all additions until it's called, // at which time _deferredAddActionCalls will be deleted. this._deferredAddActionCalls.push(() => this.addAction(action)); return action; } this._registerAction(action); for (let bpa of allBrowserPageActions()) { bpa.placeAction(action); } return action; }, _registerAction(action) { if (this.actionForID(action.id)) { throw new Error(`Action with ID '${action.id}' already added`); } this._actionsByID.set(action.id, action); // Insert the action into the appropriate list, either _builtInActions or // _nonBuiltInActions. // Keep in mind that _insertBeforeActionID may be present but null, which // means the action should be appended to the built-ins. if ("__insertBeforeActionID" in action) { // A "semi-built-in" action, probably an action from an extension // bundled with the browser. Right now we simply assume that no other // consumers will use _insertBeforeActionID. let index = !action.__insertBeforeActionID ? -1 : this._builtInActions.findIndex(a => { return a.id == action.__insertBeforeActionID; }); if (index < 0) { // Append the action (excluding transient actions). index = this._builtInActions.filter(a => !a.__transient).length; } this._builtInActions.splice(index, 0, action); } else if (action.__transient) { // A transient action. this._transientActions.push(action); } else if (action._isBuiltIn) { // A built-in action. These are mostly added on init before all other // actions, one after the other. Extension actions load later and should // be at the end, so just push onto the array. this._builtInActions.push(action); } else { // A non-built-in action, like a non-bundled extension potentially. // Keep this list sorted by title. let index = lazy.BinarySearch.insertionIndexOf( (a1, a2) => { return a1.getTitle().localeCompare(a2.getTitle()); }, this._nonBuiltInActions, action ); this._nonBuiltInActions.splice(index, 0, action); } let isNew = !this._persistedActions.ids.includes(action.id); if (isNew) { // The action is new. Store it in the persisted actions. this._persistedActions.ids.push(action.id); } // Actions are always pinned to the urlbar, except for panel separators. action._pinnedToUrlbar = !action.__isSeparator; this._updateIDsPinnedToUrlbarForAction(action); }, _updateIDsPinnedToUrlbarForAction(action) { let index = this._persistedActions.idsInUrlbar.indexOf(action.id); if (action.pinnedToUrlbar) { if (index < 0) { index = action.id == ACTION_ID_BOOKMARK ? -1 : this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK); if (index < 0) { index = this._persistedActions.idsInUrlbar.length; } this._persistedActions.idsInUrlbar.splice(index, 0, action.id); } } else if (index >= 0) { this._persistedActions.idsInUrlbar.splice(index, 1); } this._storePersistedActions(); }, // These keep track of currently registered actions. _builtInActions: [], _nonBuiltInActions: [], _transientActions: [], _actionsByID: new Map(), /** * Call this when an action is removed. * * @param action (Action object, required) * The action that was removed. */ onActionRemoved(action) { if (!this.actionForID(action.id)) { // The action isn't registered (yet). Not an error. return; } this._actionsByID.delete(action.id); let lists = [ this._builtInActions, this._nonBuiltInActions, this._transientActions, ]; for (let list of lists) { let index = list.findIndex(a => a.id == action.id); if (index >= 0) { list.splice(index, 1); break; } } for (let bpa of allBrowserPageActions()) { bpa.removeAction(action); } }, /** * Call this when an action's pinnedToUrlbar property changes. * * @param action (Action object, required) * The action whose pinnedToUrlbar property changed. */ onActionToggledPinnedToUrlbar(action) { if (!this.actionForID(action.id)) { // This may be called before the action has been added. return; } this._updateIDsPinnedToUrlbarForAction(action); for (let bpa of allBrowserPageActions()) { bpa.placeActionInUrlbar(action); } }, // For tests. See Bug 1413692. _reset() { PageActions._purgeUnregisteredPersistedActions(); PageActions._builtInActions = []; PageActions._nonBuiltInActions = []; PageActions._transientActions = []; PageActions._actionsByID = new Map(); }, _storePersistedActions() { let json = JSON.stringify(this._persistedActions); Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json); }, _loadPersistedActions() { let actions; try { let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS); actions = this._migratePersistedActions(JSON.parse(json)); } catch (ex) {} // Handle migrating to and from Proton. We want to gracefully handle // downgrades from Proton, and since Proton is controlled by a pref, we also // don't want to assume that a downgrade is possible only by downgrading the // app. That makes it hard to use the normal migration approach of creating // a new persisted actions version, so we handle Proton migration specially. // We try-catch it separately from the earlier _migratePersistedActions call // because it should not be short-circuited when the pref load or usual // migration fails. try { actions = this._migratePersistedActionsProton(actions); } catch (ex) {} // If `actions` is still not defined, then this._persistedActions will // remain its default value. if (actions) { this._persistedActions = actions; } }, _purgeUnregisteredPersistedActions() { // Remove all action IDs from persisted state that do not correspond to // currently registered actions. for (let name of ["ids", "idsInUrlbar"]) { this._persistedActions[name] = this._persistedActions[name].filter(id => { return this.actionForID(id); }); } this._storePersistedActions(); }, _migratePersistedActions(actions) { // Start with actions.version and migrate one version at a time, all the way // up to the current version. for ( let version = actions.version || 0; version < PERSISTED_ACTIONS_CURRENT_VERSION; version++ ) { let methodName = `_migratePersistedActionsTo${version + 1}`; actions = this[methodName](actions); actions.version = version + 1; } return actions; }, _migratePersistedActionsTo1(actions) { // The `ids` object is a mapping: action ID => true. Convert it to an array // to save space in the prefs. let ids = []; for (let id in actions.ids) { ids.push(id); } // Move the bookmark ID to the end of idsInUrlbar. The bookmark action // should always remain at the end of the urlbar, if present. let bookmarkIndex = actions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK); if (bookmarkIndex >= 0) { actions.idsInUrlbar.splice(bookmarkIndex, 1); actions.idsInUrlbar.push(ACTION_ID_BOOKMARK); } return { ids, idsInUrlbar: actions.idsInUrlbar, }; }, _migratePersistedActionsProton(actions) { if (actions?.idsInUrlbarPreProton) { // continue with Proton } else if (actions) { // upgrade to Proton actions.idsInUrlbarPreProton = [...(actions.idsInUrlbar || [])]; } else { // new profile with Proton actions = { ids: [], idsInUrlbar: [], idsInUrlbarPreProton: [], version: PERSISTED_ACTIONS_CURRENT_VERSION, }; } return actions; }, /** * Send an ASRouter trigger to possibly show messaging related to the page * action that was placed in the urlbar. * * @param {Element} buttonNode The page action button node. */ sendPlacedInUrlbarTrigger(buttonNode) { lazy.setTimeout(async () => { await lazy.ASRouter.initialized; let win = buttonNode?.ownerGlobal; if (!win || buttonNode.hidden) { return; } await lazy.ASRouter.sendTriggerMessage({ browser: win.gBrowser.selectedBrowser, id: "pageActionInUrlbar", context: { pageAction: buttonNode.id }, }); }, 500); }, // This keeps track of all actions, even those that are not currently // registered because they have been removed, so long as // _purgeUnregisteredPersistedActions has not been called. _persistedActions: { version: PERSISTED_ACTIONS_CURRENT_VERSION, // action IDs that have ever been seen and not removed, order not important ids: [], // action IDs ordered by position in urlbar idsInUrlbar: [], }, }; /** * A single page action. * * Each action can have both per-browser-window state and global state. * Per-window state takes precedence over global state. This is reflected in * the title, tooltip, disabled, and icon properties. Each of these properties * has a getter method and setter method that takes a browser window. Pass null * to get the action's global state. Pass a browser window to get the per- * window state. However, if you pass a window and the action has no state for * that window, then the global state will be returned. * * `options` is a required object with the following properties. Regarding the * properties discussed in the previous paragraph, the values in `options` set * global state. * * @param id (string, required) * The action's ID. Treat this like the ID of a DOM node. * @param title (string, optional) * The action's title. It is optional for built in actions. * @param anchorIDOverride (string, optional) * Pass a string to override the node to which the action's activated- * action panel is anchored. * @param disabled (bool, optional) * Pass true to cause the action to be disabled initially in all browser * windows. False by default. * @param extensionID (string, optional) * If the action lives in an extension, pass its ID. * @param iconURL (string or object, optional) * The URL string of the action's icon. Usually you want to specify an * icon in CSS, but this option is useful if that would be a pain for * some reason. You can also pass an object that maps pixel sizes to * URLs, like { 16: url16, 32: url32 }. The best size for the user's * screen will be used. * @param isBadged (bool, optional) * If true, the toolbarbutton for this action will get a * "badged" attribute. * @param onBeforePlacedInWindow (function, optional) * Called before the action is placed in the window: * onBeforePlacedInWindow(window) * * window: The window that the action will be placed in. * @param onCommand (function, optional) * Called when the action is clicked, but only if it has neither a * subview nor an iframe: * onCommand(event, buttonNode) * * event: The triggering event. * * buttonNode: The button node that was clicked. * @param onIframeHiding (function, optional) * Called when the action's iframe is hiding: * onIframeHiding(iframeNode, parentPanelNode) * * iframeNode: The iframe. * * parentPanelNode: The panel node in which the iframe is shown. * @param onIframeHidden (function, optional) * Called when the action's iframe is hidden: * onIframeHidden(iframeNode, parentPanelNode) * * iframeNode: The iframe. * * parentPanelNode: The panel node in which the iframe is shown. * @param onIframeShowing (function, optional) * Called when the action's iframe is showing to the user: * onIframeShowing(iframeNode, parentPanelNode) * * iframeNode: The iframe. * * parentPanelNode: The panel node in which the iframe is shown. * @param onLocationChange (function, optional) * Called after tab switch or when the current 's location * changes: * onLocationChange(browserWindow) * * browserWindow: The browser window containing the tab switch or * changed . * @param onPlacedInPanel (function, optional) * Called when the action is added to the page action panel in a browser * window: * onPlacedInPanel(buttonNode) * * buttonNode: The action's node in the page action panel. * @param onPlacedInUrlbar (function, optional) * Called when the action is added to the urlbar in a browser window: * onPlacedInUrlbar(buttonNode) * * buttonNode: The action's node in the urlbar. * @param onRemovedFromWindow (function, optional) * Called after the action is removed from a browser window: * onRemovedFromWindow(browserWindow) * * browserWindow: The browser window that the action was removed from. * @param onShowingInPanel (function, optional) * Called when a browser window's page action panel is showing: * onShowingInPanel(buttonNode) * * buttonNode: The action's node in the page action panel. * @param onSubviewPlaced (function, optional) * Called when the action's subview is added to its parent panel in a * browser window: * onSubviewPlaced(panelViewNode) * * panelViewNode: The subview's panelview node. * @param onSubviewShowing (function, optional) * Called when the action's subview is showing in a browser window: * onSubviewShowing(panelViewNode) * * panelViewNode: The subview's panelview node. * @param pinnedToUrlbar (bool, optional) * Pass true to pin the action to the urlbar. An action is shown in the * urlbar if it's pinned and not disabled. False by default. * @param tooltip (string, optional) * The action's button tooltip text. * @param urlbarIDOverride (string, optional) * Usually the ID of the action's button in the urlbar will be generated * automatically. Pass a string for this property to override that with * your own ID. * @param wantsIframe (bool, optional) * Pass true to make an action that shows an iframe in a panel when * clicked. * @param wantsSubview (bool, optional) * Pass true to make an action that shows a panel subview when clicked. * @param disablePrivateBrowsing (bool, optional) * Pass true to prevent the action from showing in a private browsing window. */ function Action(options) { setProperties(this, options, { id: true, title: false, anchorIDOverride: false, disabled: false, extensionID: false, iconURL: false, isBadged: false, labelForHistogram: false, onBeforePlacedInWindow: false, onCommand: false, onIframeHiding: false, onIframeHidden: false, onIframeShowing: false, onLocationChange: false, onPlacedInPanel: false, onPlacedInUrlbar: false, onRemovedFromWindow: false, onShowingInPanel: false, onSubviewPlaced: false, onSubviewShowing: false, onPinToUrlbarToggled: false, pinnedToUrlbar: false, tooltip: false, urlbarIDOverride: false, wantsIframe: false, wantsSubview: false, disablePrivateBrowsing: false, // private // (string, optional) // The ID of another action before which to insert this new action in the // panel. _insertBeforeActionID: false, // (bool, optional) // True if this isn't really an action but a separator to be shown in the // page action panel. _isSeparator: false, // (bool, optional) // Transient actions have a couple of special properties: (1) They stick to // the bottom of the panel, and (2) they're hidden in the panel when they're // disabled. Other than that they behave like other actions. _transient: false, // (bool, optional) // True if the action's urlbar button is defined in markup. In that case, a // node with the action's urlbar node ID should already exist in the DOM // (either the auto-generated ID or urlbarIDOverride). That node will be // shown when the action is added to the urlbar and hidden when the action // is removed from the urlbar. _urlbarNodeInMarkup: false, }); /** * A cache of the pre-computed CSS variable values for a given icon * URLs object, as passed to _createIconProperties. */ this._iconProperties = new WeakMap(); /** * The global values for the action properties. */ this._globalProps = { disabled: this._disabled, iconURL: this._iconURL, iconProps: this._createIconProperties(this._iconURL), title: this._title, tooltip: this._tooltip, wantsSubview: this._wantsSubview, }; /** * A mapping of window-specific action property objects, each of which * derives from the _globalProps object. */ this._windowProps = new WeakMap(); } Action.prototype = { /** * The ID of the action's parent extension (string) */ get extensionID() { return this._extensionID; }, /** * The action's ID (string) */ get id() { return this._id; }, get disablePrivateBrowsing() { return !!this._disablePrivateBrowsing; }, /** * Verifies that the action can be shown in a private window. For * extensions, verifies the extension has access to the window. */ canShowInWindow(browserWindow) { if (this._extensionID) { let policy = WebExtensionPolicy.getByID(this._extensionID); if (!policy.canAccessWindow(browserWindow)) { return false; } } return !( this.disablePrivateBrowsing && lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow) ); }, /** * True if the action is pinned to the urlbar. The action is shown in the * urlbar if it's pinned and not disabled. (bool) */ get pinnedToUrlbar() { return this._pinnedToUrlbar || false; }, set pinnedToUrlbar(shown) { if (this.pinnedToUrlbar != shown) { this._pinnedToUrlbar = shown; PageActions.onActionToggledPinnedToUrlbar(this); this.onPinToUrlbarToggled(); } }, /** * The action's disabled state (bool) */ getDisabled(browserWindow = null) { return !!this._getProperties(browserWindow).disabled; }, setDisabled(value, browserWindow = null) { return this._setProperty("disabled", !!value, browserWindow); }, /** * The action's icon URL string, or an object mapping sizes to URL strings * (string or object) */ getIconURL(browserWindow = null) { return this._getProperties(browserWindow).iconURL; }, setIconURL(value, browserWindow = null) { let props = this._getProperties(browserWindow, !!browserWindow); props.iconURL = value; props.iconProps = this._createIconProperties(value); this._updateProperty("iconURL", props.iconProps, browserWindow); return value; }, /** * The set of CSS variables which define the action's icons in various * sizes. This is generated automatically from the iconURL property. */ getIconProperties(browserWindow = null) { return this._getProperties(browserWindow).iconProps; }, _createIconProperties(urls) { if (urls && typeof urls == "object") { let props = this._iconProperties.get(urls); if (!props) { props = Object.freeze({ "--pageAction-image": `image-set( ${escapeCSSURL(this._iconURLForSize(urls, 16))}, ${escapeCSSURL(this._iconURLForSize(urls, 32))} 2x )`, }); this._iconProperties.set(urls, props); } return props; } let cssURL = urls ? escapeCSSURL(urls) : null; return Object.freeze({ "--pageAction-image": cssURL, }); }, /** * The action's title (string). Note, built in actions will * not have a title property. */ getTitle(browserWindow = null) { return this._getProperties(browserWindow).title; }, setTitle(value, browserWindow = null) { return this._setProperty("title", value, browserWindow); }, /** * The action's tooltip (string) */ getTooltip(browserWindow = null) { return this._getProperties(browserWindow).tooltip; }, setTooltip(value, browserWindow = null) { return this._setProperty("tooltip", value, browserWindow); }, /** * Whether the action wants a subview (bool) */ getWantsSubview(browserWindow = null) { return !!this._getProperties(browserWindow).wantsSubview; }, setWantsSubview(value, browserWindow = null) { return this._setProperty("wantsSubview", !!value, browserWindow); }, /** * Sets a property, optionally for a particular browser window. * * @param name (string, required) * The (non-underscored) name of the property. * @param value * The value. * @param browserWindow (DOM window, optional) * If given, then the property will be set in this window's state, not * globally. */ _setProperty(name, value, browserWindow) { let props = this._getProperties(browserWindow, !!browserWindow); props[name] = value; this._updateProperty(name, value, browserWindow); return value; }, _updateProperty(name, value, browserWindow) { // This may be called before the action has been added. if (PageActions.actionForID(this.id)) { for (let bpa of allBrowserPageActions(browserWindow)) { bpa.updateAction(this, name, { value }); } } }, /** * Returns the properties object for the given window, if it exists, * or the global properties object if no window-specific properties * exist. * * @param {Window?} window * The window for which to return the properties object, or * null to return the global properties object. * @param {bool} [forceWindowSpecific = false] * If true, always returns a window-specific properties object. * If a properties object does not exist for the given window, * one is created and cached. * @returns {object} */ _getProperties(window, forceWindowSpecific = false) { let props = window && this._windowProps.get(window); if (!props && forceWindowSpecific) { props = Object.create(this._globalProps); this._windowProps.set(window, props); } return props || this._globalProps; }, /** * Override for the ID of the action's activated-action panel anchor (string) */ get anchorIDOverride() { return this._anchorIDOverride; }, /** * Override for the ID of the action's urlbar node (string) */ get urlbarIDOverride() { return this._urlbarIDOverride; }, /** * True if the action is shown in an iframe (bool) */ get wantsIframe() { return this._wantsIframe || false; }, get isBadged() { return this._isBadged || false; }, get labelForHistogram() { // The histogram label value has a length limit of 20 and restricted to a // pattern. See MAX_LABEL_LENGTH and CPP_IDENTIFIER_PATTERN in // toolkit/components/telemetry/parse_histograms.py return ( this._labelForHistogram || this._id.replace(/_\w{1}/g, match => match[1].toUpperCase()).substr(0, 20) ); }, /** * Selects the best matching icon from the given URLs object for the * given preferred size. * * @param {object} urls * An object containing square icons of various sizes. The name * of each property is its width, and the value is its image URL. * @param {integer} peferredSize * The preferred icon width. The most appropriate icon in the * urls object will be chosen to match that size. An exact * match will be preferred, followed by an icon exactly double * the size, followed by the smallest icon larger than the * preferred size, followed by the largest available icon. * @returns {string} * The chosen icon URL. */ _iconURLForSize(urls, preferredSize) { // This case is copied from ExtensionParent.sys.mjs so that our image logic is // the same, so that WebExtensions page action tests that deal with icons // pass. let bestSize = null; if (urls[preferredSize]) { bestSize = preferredSize; } else if (urls[2 * preferredSize]) { bestSize = 2 * preferredSize; } else { let sizes = Object.keys(urls) .map(key => parseInt(key, 10)) .sort((a, b) => a - b); bestSize = sizes.find(candidate => candidate > preferredSize) || sizes.pop(); } return urls[bestSize]; }, /** * Performs the command for an action. If the action has an onCommand * handler, then it's called. If the action has a subview or iframe, then a * panel is opened, displaying the subview or iframe. * * @param browserWindow (DOM window, required) * The browser window in which to perform the action. */ doCommand(browserWindow) { browserPageActions(browserWindow).doCommandForAction(this); }, /** * Call this when before placing the action in the window. * * @param browserWindow (DOM window, required) * The browser window the action will be placed in. */ onBeforePlacedInWindow(browserWindow) { if (this._onBeforePlacedInWindow) { this._onBeforePlacedInWindow(browserWindow); } }, /** * Call this when the user activates the action. * * @param event (DOM event, required) * The triggering event. * @param buttonNode (DOM node, required) * The action's panel or urlbar button node that was clicked. */ onCommand(event, buttonNode) { if (this._onCommand) { this._onCommand(event, buttonNode); } }, /** * Call this when the action's iframe is hiding. * * @param iframeNode (DOM node, required) * The iframe that's hiding. * @param parentPanelNode (DOM node, required) * The panel in which the iframe is hiding. */ onIframeHiding(iframeNode, parentPanelNode) { if (this._onIframeHiding) { this._onIframeHiding(iframeNode, parentPanelNode); } }, /** * Call this when the action's iframe is hidden. * * @param iframeNode (DOM node, required) * The iframe that's being hidden. * @param parentPanelNode (DOM node, required) * The panel in which the iframe is hidden. */ onIframeHidden(iframeNode, parentPanelNode) { if (this._onIframeHidden) { this._onIframeHidden(iframeNode, parentPanelNode); } }, /** * Call this when the action's iframe is showing. * * @param iframeNode (DOM node, required) * The iframe that's being shown. * @param parentPanelNode (DOM node, required) * The panel in which the iframe is shown. */ onIframeShowing(iframeNode, parentPanelNode) { if (this._onIframeShowing) { this._onIframeShowing(iframeNode, parentPanelNode); } }, /** * Call this on tab switch or when the current 's location changes. * * @param browserWindow (DOM window, required) * The browser window containing the tab switch or changed . */ onLocationChange(browserWindow) { if (this._onLocationChange) { this._onLocationChange(browserWindow); } }, /** * Call this when a DOM node for the action is added to the page action panel. * * @param buttonNode (DOM node, required) * The action's panel button node. */ onPlacedInPanel(buttonNode) { if (this._onPlacedInPanel) { this._onPlacedInPanel(buttonNode); } }, /** * Call this when a DOM node for the action is added to the urlbar. * * @param buttonNode (DOM node, required) * The action's urlbar button node. */ onPlacedInUrlbar(buttonNode) { if (this._onPlacedInUrlbar) { this._onPlacedInUrlbar(buttonNode); } }, /** * Call this when the DOM nodes for the action are removed from a browser * window. * * @param browserWindow (DOM window, required) * The browser window the action was removed from. */ onRemovedFromWindow(browserWindow) { if (this._onRemovedFromWindow) { this._onRemovedFromWindow(browserWindow); } }, /** * Call this when the action's button is shown in the page action panel. * * @param buttonNode (DOM node, required) * The action's panel button node. */ onShowingInPanel(buttonNode) { if (this._onShowingInPanel) { this._onShowingInPanel(buttonNode); } }, /** * Call this when a panelview node for the action's subview is added to the * DOM. * * @param panelViewNode (DOM node, required) * The subview's panelview node. */ onSubviewPlaced(panelViewNode) { if (this._onSubviewPlaced) { this._onSubviewPlaced(panelViewNode); } }, /** * Call this when a panelview node for the action's subview is showing. * * @param panelViewNode (DOM node, required) * The subview's panelview node. */ onSubviewShowing(panelViewNode) { if (this._onSubviewShowing) { this._onSubviewShowing(panelViewNode); } }, /** * Call this when an icon in the url is pinned or unpinned. */ onPinToUrlbarToggled() { if (this._onPinToUrlbarToggled) { this._onPinToUrlbarToggled(); } }, /** * Removes the action's DOM nodes from all browser windows. * * PageActions will remember the action's urlbar placement, if any, after this * method is called until app shutdown. If the action is not added again * before shutdown, then PageActions will discard the placement, and the next * time the action is added, its placement will be reset. */ remove() { PageActions.onActionRemoved(this); }, /** * Returns whether the action should be shown in a given window's panel. * * @param browserWindow (DOM window, required) * The window. * @return True if the action should be shown and false otherwise. Actions * are always shown in the panel unless they're both transient and * disabled. */ shouldShowInPanel(browserWindow) { // When Proton is enabled, the extension page actions should behave similarly // to a transient action, and be hidden from the urlbar overflow menu if they // are disabled (as in the urlbar when the overflow menu isn't available) // // TODO(Bug 1704139): as a follow up we may look into just set on all // extensions pageActions `_transient: true`, at least once we sunset // the proton preference and we don't need the pre-Proton behavior anymore, // and remove this special case. const isProtonExtensionAction = this.extensionID; return ( (!(this.__transient || isProtonExtensionAction) || !this.getDisabled(browserWindow)) && this.canShowInWindow(browserWindow) ); }, /** * Returns whether the action should be shown in a given window's urlbar. * * @param browserWindow (DOM window, required) * The window. * @return True if the action should be shown and false otherwise. The action * should be shown if it's both pinned and not disabled. */ shouldShowInUrlbar(browserWindow) { return ( this.pinnedToUrlbar && !this.getDisabled(browserWindow) && this.canShowInWindow(browserWindow) ); }, get _isBuiltIn() { let builtInIDs = ["screenshots_mozilla_org"].concat( gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id) ); return builtInIDs.includes(this.id); }, get _isMozillaAction() { return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org"; }, }; PageActions.Action = Action; PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR; PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR; // These are only necessary so that the test can use them. PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK; PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS; // Sorted in the order in which they should appear in the page action panel. // Does not include the page actions of extensions bundled with the browser. // They're added by the relevant extension code. // NOTE: If you add items to this list (or system add-on actions that we // want to keep track of), make sure to also update Histograms.json for the // new actions. var gBuiltInActions; PageActions._initBuiltInActions = function () { gBuiltInActions = [ // bookmark { id: ACTION_ID_BOOKMARK, urlbarIDOverride: "star-button-box", _urlbarNodeInMarkup: true, pinnedToUrlbar: true, onShowingInPanel(buttonNode) { browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode); }, onCommand(event, buttonNode) { browserPageActions(buttonNode).bookmark.onCommand(event, buttonNode); }, }, ]; }; /** * Gets a BrowserPageActions object in a browser window. * * @param obj * Either a DOM node or a browser window. * @return The BrowserPageActions object in the browser window related to the * given object. */ function browserPageActions(obj) { if (obj.BrowserPageActions) { return obj.BrowserPageActions; } return obj.ownerGlobal.BrowserPageActions; } /** * A generator function for all open browser windows. * * @param browserWindow (DOM window, optional) * If given, then only this window will be yielded. That may sound * pointless, but it can make callers nicer to write since they don't * need two separate cases, one where a window is given and another where * it isn't. */ function* allBrowserWindows(browserWindow = null) { if (browserWindow) { yield browserWindow; return; } yield* Services.wm.getEnumerator("navigator:browser"); } /** * A generator function for BrowserPageActions objects in all open windows. * * @param browserWindow (DOM window, optional) * If given, then the BrowserPageActions for only this window will be * yielded. */ function* allBrowserPageActions(browserWindow = null) { for (let win of allBrowserWindows(browserWindow)) { yield browserPageActions(win); } } /** * A simple function that sets properties on a given object while doing basic * required-properties checking. If a required property isn't specified in the * given options object, or if the options object has properties that aren't in * the given schema, then an error is thrown. * * @param obj * The object to set properties on. * @param options * An options object supplied by the consumer. * @param schema * An object a property for each required and optional property. The * keys are property names; the value of a key is a bool that is true if * the property is required. */ function setProperties(obj, options, schema) { for (let name in schema) { let required = schema[name]; if (required && !(name in options)) { throw new Error(`'${name}' must be specified`); } let nameInObj = "_" + name; if (name[0] == "_") { // The property is "private". If it's defined in the options, then define // it on obj exactly as it's defined on options. if (name in options) { obj[nameInObj] = options[name]; } } else { // The property is "public". Make sure the property is defined on obj. obj[nameInObj] = options[name] || null; } } for (let name in options) { if (!(name in schema)) { throw new Error(`Unrecognized option '${name}'`); } } }