tor-browser

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

PageActions.sys.mjs (42388B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
      9  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     10  BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs",
     11  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     12  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     13 });
     14 
     15 const ACTION_ID_BOOKMARK = "bookmark";
     16 const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
     17 const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
     18 
     19 const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
     20 const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
     21 
     22 // Escapes the given raw URL string, and returns an equivalent CSS url()
     23 // value for it.
     24 function escapeCSSURL(url) {
     25  return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
     26 }
     27 
     28 export var PageActions = {
     29  /**
     30   * Initializes PageActions.
     31   *
     32   * @param {boolean} addShutdownBlocker
     33   *   This param exists only for tests.  Normally the default value of true
     34   *   must be used.
     35   */
     36  init(addShutdownBlocker = true) {
     37    this._initBuiltInActions();
     38 
     39    let callbacks = this._deferredAddActionCalls;
     40    delete this._deferredAddActionCalls;
     41 
     42    this._loadPersistedActions();
     43 
     44    // Register the built-in actions, which are defined below in this file.
     45    for (let options of gBuiltInActions) {
     46      if (!this.actionForID(options.id)) {
     47        this._registerAction(new Action(options));
     48      }
     49    }
     50 
     51    // Now place them all in each window.  Instead of splitting the register and
     52    // place steps, we could simply call addAction, which does both, but doing
     53    // it this way means that all windows initially place their actions in the
     54    // urlbar the same way -- placeAllActions -- regardless of whether they're
     55    // open when this method is called or opened later.
     56    for (let bpa of allBrowserPageActions()) {
     57      bpa.placeAllActionsInUrlbar();
     58    }
     59 
     60    // These callbacks are deferred until init happens and all built-in actions
     61    // are added.
     62    while (callbacks && callbacks.length) {
     63      callbacks.shift()();
     64    }
     65 
     66    if (addShutdownBlocker) {
     67      // Purge removed actions from persisted state on shutdown.  The point is
     68      // not to do it on Action.remove().  That way actions that are removed and
     69      // re-added while the app is running will have their urlbar placement and
     70      // other state remembered and restored.  This happens for upgraded and
     71      // downgraded extensions, for example.
     72      lazy.AsyncShutdown.profileBeforeChange.addBlocker(
     73        "PageActions: purging unregistered actions from cache",
     74        () => this._purgeUnregisteredPersistedActions()
     75      );
     76    }
     77  },
     78 
     79  _deferredAddActionCalls: [],
     80 
     81  /**
     82   * A list of all Action objects, not in any particular order.  Not live.
     83   * (array of Action objects)
     84   */
     85  get actions() {
     86    let lists = [
     87      this._builtInActions,
     88      this._nonBuiltInActions,
     89      this._transientActions,
     90    ];
     91    return lists.reduce((memo, list) => memo.concat(list), []);
     92  },
     93 
     94  /**
     95   * The list of Action objects that should appear in the panel for a given
     96   * window, sorted in the order in which they appear.  If there are both
     97   * built-in and non-built-in actions, then the list will include the separator
     98   * between the two.  The list is not live.  (array of Action objects)
     99   *
    100   * @param  browserWindow (DOM window, required)
    101   *         This window's actions will be returned.
    102   * @return (array of PageAction.Action objects) The actions currently in the
    103   *         given window's panel.
    104   */
    105  actionsInPanel(browserWindow) {
    106    function filter(action) {
    107      return action.shouldShowInPanel(browserWindow);
    108    }
    109    let actions = this._builtInActions.filter(filter);
    110    let nonBuiltInActions = this._nonBuiltInActions.filter(filter);
    111    if (nonBuiltInActions.length) {
    112      if (actions.length) {
    113        actions.push(
    114          new Action({
    115            id: ACTION_ID_BUILT_IN_SEPARATOR,
    116            _isSeparator: true,
    117          })
    118        );
    119      }
    120      actions.push(...nonBuiltInActions);
    121    }
    122    let transientActions = this._transientActions.filter(filter);
    123    if (transientActions.length) {
    124      if (actions.length) {
    125        actions.push(
    126          new Action({
    127            id: ACTION_ID_TRANSIENT_SEPARATOR,
    128            _isSeparator: true,
    129          })
    130        );
    131      }
    132      actions.push(...transientActions);
    133    }
    134    return actions;
    135  },
    136 
    137  /**
    138   * The list of actions currently in the urlbar, sorted in the order in which
    139   * they appear.  Not live.
    140   *
    141   * @param  browserWindow (DOM window, required)
    142   *         This window's actions will be returned.
    143   * @return (array of PageAction.Action objects) The actions currently in the
    144   *         given window's urlbar.
    145   */
    146  actionsInUrlbar(browserWindow) {
    147    // Remember that IDs in idsInUrlbar may belong to actions that aren't
    148    // currently registered.
    149    return this._persistedActions.idsInUrlbar.reduce((actions, id) => {
    150      let action = this.actionForID(id);
    151      if (action && action.shouldShowInUrlbar(browserWindow)) {
    152        actions.push(action);
    153      }
    154      return actions;
    155    }, []);
    156  },
    157 
    158  /**
    159   * Gets an action.
    160   *
    161   * @param  id (string, required)
    162   *         The ID of the action to get.
    163   * @return The Action object, or null if none.
    164   */
    165  actionForID(id) {
    166    return this._actionsByID.get(id);
    167  },
    168 
    169  /**
    170   * Registers an action.
    171   *
    172   * Actions are registered by their IDs.  An error is thrown if an action with
    173   * the given ID has already been added.  Use actionForID() before calling this
    174   * method if necessary.
    175   *
    176   * Be sure to call remove() on the action if the lifetime of the code that
    177   * owns it is shorter than the browser's -- if it lives in an extension, for
    178   * example.
    179   *
    180   * @param  action (Action, required)
    181   *         The Action object to register.
    182   * @return The given Action.
    183   */
    184  addAction(action) {
    185    if (this._deferredAddActionCalls) {
    186      // init() hasn't been called yet.  Defer all additions until it's called,
    187      // at which time _deferredAddActionCalls will be deleted.
    188      this._deferredAddActionCalls.push(() => this.addAction(action));
    189      return action;
    190    }
    191    this._registerAction(action);
    192    for (let bpa of allBrowserPageActions()) {
    193      bpa.placeAction(action);
    194    }
    195    return action;
    196  },
    197 
    198  _registerAction(action) {
    199    if (this.actionForID(action.id)) {
    200      throw new Error(`Action with ID '${action.id}' already added`);
    201    }
    202    this._actionsByID.set(action.id, action);
    203 
    204    // Insert the action into the appropriate list, either _builtInActions or
    205    // _nonBuiltInActions.
    206 
    207    // Keep in mind that _insertBeforeActionID may be present but null, which
    208    // means the action should be appended to the built-ins.
    209    if ("__insertBeforeActionID" in action) {
    210      // A "semi-built-in" action, probably an action from an extension
    211      // bundled with the browser.  Right now we simply assume that no other
    212      // consumers will use _insertBeforeActionID.
    213      let index = !action.__insertBeforeActionID
    214        ? -1
    215        : this._builtInActions.findIndex(a => {
    216            return a.id == action.__insertBeforeActionID;
    217          });
    218      if (index < 0) {
    219        // Append the action (excluding transient actions).
    220        index = this._builtInActions.filter(a => !a.__transient).length;
    221      }
    222      this._builtInActions.splice(index, 0, action);
    223    } else if (action.__transient) {
    224      // A transient action.
    225      this._transientActions.push(action);
    226    } else if (action._isBuiltIn) {
    227      // A built-in action. These are mostly added on init before all other
    228      // actions, one after the other. Extension actions load later and should
    229      // be at the end, so just push onto the array.
    230      this._builtInActions.push(action);
    231    } else {
    232      // A non-built-in action, like a non-bundled extension potentially.
    233      // Keep this list sorted by title.
    234      let index = lazy.BinarySearch.insertionIndexOf(
    235        (a1, a2) => {
    236          return a1.getTitle().localeCompare(a2.getTitle());
    237        },
    238        this._nonBuiltInActions,
    239        action
    240      );
    241      this._nonBuiltInActions.splice(index, 0, action);
    242    }
    243 
    244    let isNew = !this._persistedActions.ids.includes(action.id);
    245    if (isNew) {
    246      // The action is new.  Store it in the persisted actions.
    247      this._persistedActions.ids.push(action.id);
    248    }
    249 
    250    // Actions are always pinned to the urlbar, except for panel separators.
    251    action._pinnedToUrlbar = !action.__isSeparator;
    252    this._updateIDsPinnedToUrlbarForAction(action);
    253  },
    254 
    255  _updateIDsPinnedToUrlbarForAction(action) {
    256    let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
    257    if (action.pinnedToUrlbar) {
    258      if (index < 0) {
    259        index =
    260          action.id == ACTION_ID_BOOKMARK
    261            ? -1
    262            : this._persistedActions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
    263        if (index < 0) {
    264          index = this._persistedActions.idsInUrlbar.length;
    265        }
    266        this._persistedActions.idsInUrlbar.splice(index, 0, action.id);
    267      }
    268    } else if (index >= 0) {
    269      this._persistedActions.idsInUrlbar.splice(index, 1);
    270    }
    271    this._storePersistedActions();
    272  },
    273 
    274  // These keep track of currently registered actions.
    275  _builtInActions: [],
    276  _nonBuiltInActions: [],
    277  _transientActions: [],
    278  _actionsByID: new Map(),
    279 
    280  /**
    281   * Call this when an action is removed.
    282   *
    283   * @param  action (Action object, required)
    284   *         The action that was removed.
    285   */
    286  onActionRemoved(action) {
    287    if (!this.actionForID(action.id)) {
    288      // The action isn't registered (yet).  Not an error.
    289      return;
    290    }
    291 
    292    this._actionsByID.delete(action.id);
    293    let lists = [
    294      this._builtInActions,
    295      this._nonBuiltInActions,
    296      this._transientActions,
    297    ];
    298    for (let list of lists) {
    299      let index = list.findIndex(a => a.id == action.id);
    300      if (index >= 0) {
    301        list.splice(index, 1);
    302        break;
    303      }
    304    }
    305 
    306    for (let bpa of allBrowserPageActions()) {
    307      bpa.removeAction(action);
    308    }
    309  },
    310 
    311  /**
    312   * Call this when an action's pinnedToUrlbar property changes.
    313   *
    314   * @param  action (Action object, required)
    315   *         The action whose pinnedToUrlbar property changed.
    316   */
    317  onActionToggledPinnedToUrlbar(action) {
    318    if (!this.actionForID(action.id)) {
    319      // This may be called before the action has been added.
    320      return;
    321    }
    322    this._updateIDsPinnedToUrlbarForAction(action);
    323    for (let bpa of allBrowserPageActions()) {
    324      bpa.placeActionInUrlbar(action);
    325    }
    326  },
    327 
    328  // For tests.  See Bug 1413692.
    329  _reset() {
    330    PageActions._purgeUnregisteredPersistedActions();
    331    PageActions._builtInActions = [];
    332    PageActions._nonBuiltInActions = [];
    333    PageActions._transientActions = [];
    334    PageActions._actionsByID = new Map();
    335  },
    336 
    337  _storePersistedActions() {
    338    let json = JSON.stringify(this._persistedActions);
    339    Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
    340  },
    341 
    342  _loadPersistedActions() {
    343    let actions;
    344    try {
    345      let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
    346      actions = this._migratePersistedActions(JSON.parse(json));
    347    } catch (ex) {}
    348 
    349    // Handle migrating to and from Proton.  We want to gracefully handle
    350    // downgrades from Proton, and since Proton is controlled by a pref, we also
    351    // don't want to assume that a downgrade is possible only by downgrading the
    352    // app.  That makes it hard to use the normal migration approach of creating
    353    // a new persisted actions version, so we handle Proton migration specially.
    354    // We try-catch it separately from the earlier _migratePersistedActions call
    355    // because it should not be short-circuited when the pref load or usual
    356    // migration fails.
    357    try {
    358      actions = this._migratePersistedActionsProton(actions);
    359    } catch (ex) {}
    360 
    361    // If `actions` is still not defined, then this._persistedActions will
    362    // remain its default value.
    363    if (actions) {
    364      this._persistedActions = actions;
    365    }
    366  },
    367 
    368  _purgeUnregisteredPersistedActions() {
    369    // Remove all action IDs from persisted state that do not correspond to
    370    // currently registered actions.
    371    for (let name of ["ids", "idsInUrlbar"]) {
    372      this._persistedActions[name] = this._persistedActions[name].filter(id => {
    373        return this.actionForID(id);
    374      });
    375    }
    376    this._storePersistedActions();
    377  },
    378 
    379  _migratePersistedActions(actions) {
    380    // Start with actions.version and migrate one version at a time, all the way
    381    // up to the current version.
    382    for (
    383      let version = actions.version || 0;
    384      version < PERSISTED_ACTIONS_CURRENT_VERSION;
    385      version++
    386    ) {
    387      let methodName = `_migratePersistedActionsTo${version + 1}`;
    388      actions = this[methodName](actions);
    389      actions.version = version + 1;
    390    }
    391    return actions;
    392  },
    393 
    394  _migratePersistedActionsTo1(actions) {
    395    // The `ids` object is a mapping: action ID => true.  Convert it to an array
    396    // to save space in the prefs.
    397    let ids = [];
    398    for (let id in actions.ids) {
    399      ids.push(id);
    400    }
    401    // Move the bookmark ID to the end of idsInUrlbar.  The bookmark action
    402    // should always remain at the end of the urlbar, if present.
    403    let bookmarkIndex = actions.idsInUrlbar.indexOf(ACTION_ID_BOOKMARK);
    404    if (bookmarkIndex >= 0) {
    405      actions.idsInUrlbar.splice(bookmarkIndex, 1);
    406      actions.idsInUrlbar.push(ACTION_ID_BOOKMARK);
    407    }
    408    return {
    409      ids,
    410      idsInUrlbar: actions.idsInUrlbar,
    411    };
    412  },
    413 
    414  _migratePersistedActionsProton(actions) {
    415    if (actions?.idsInUrlbarPreProton) {
    416      // continue with Proton
    417    } else if (actions) {
    418      // upgrade to Proton
    419      actions.idsInUrlbarPreProton = [...(actions.idsInUrlbar || [])];
    420    } else {
    421      // new profile with Proton
    422      actions = {
    423        ids: [],
    424        idsInUrlbar: [],
    425        idsInUrlbarPreProton: [],
    426        version: PERSISTED_ACTIONS_CURRENT_VERSION,
    427      };
    428    }
    429    return actions;
    430  },
    431 
    432  /**
    433   * Send an ASRouter trigger to possibly show messaging related to the page
    434   * action that was placed in the urlbar.
    435   *
    436   * @param {Element} buttonNode The page action button node.
    437   */
    438  sendPlacedInUrlbarTrigger(buttonNode) {
    439    lazy.setTimeout(async () => {
    440      await lazy.ASRouter.waitForInitialized;
    441      let win = buttonNode?.ownerGlobal;
    442      if (!win || buttonNode.hidden) {
    443        return;
    444      }
    445      await lazy.ASRouter.sendTriggerMessage({
    446        browser: win.gBrowser.selectedBrowser,
    447        id: "pageActionInUrlbar",
    448        context: { pageAction: buttonNode.id },
    449      });
    450    }, 500);
    451  },
    452 
    453  // This keeps track of all actions, even those that are not currently
    454  // registered because they have been removed, so long as
    455  // _purgeUnregisteredPersistedActions has not been called.
    456  _persistedActions: {
    457    version: PERSISTED_ACTIONS_CURRENT_VERSION,
    458    // action IDs that have ever been seen and not removed, order not important
    459    ids: [],
    460    // action IDs ordered by position in urlbar
    461    idsInUrlbar: [],
    462  },
    463 };
    464 
    465 /**
    466 * A single page action.
    467 *
    468 * Each action can have both per-browser-window state and global state.
    469 * Per-window state takes precedence over global state.  This is reflected in
    470 * the title, tooltip, disabled, and icon properties.  Each of these properties
    471 * has a getter method and setter method that takes a browser window.  Pass null
    472 * to get the action's global state.  Pass a browser window to get the per-
    473 * window state.  However, if you pass a window and the action has no state for
    474 * that window, then the global state will be returned.
    475 *
    476 * `options` is a required object with the following properties.  Regarding the
    477 * properties discussed in the previous paragraph, the values in `options` set
    478 * global state.
    479 *
    480 * @param id (string, required)
    481 *        The action's ID.  Treat this like the ID of a DOM node.
    482 * @param title (string, optional)
    483 *        The action's title. It is optional for built in actions.
    484 * @param anchorIDOverride (string, optional)
    485 *        Pass a string to override the node to which the action's activated-
    486 *        action panel is anchored.
    487 * @param disabled (bool, optional)
    488 *        Pass true to cause the action to be disabled initially in all browser
    489 *        windows.  False by default.
    490 * @param extensionID (string, optional)
    491 *        If the action lives in an extension, pass its ID.
    492 * @param iconURL (string or object, optional)
    493 *        The URL string of the action's icon.  Usually you want to specify an
    494 *        icon in CSS, but this option is useful if that would be a pain for
    495 *        some reason.  You can also pass an object that maps pixel sizes to
    496 *        URLs, like { 16: url16, 32: url32 }.  The best size for the user's
    497 *        screen will be used.
    498 * @param isBadged (bool, optional)
    499 *        If true, the toolbarbutton for this action will get a
    500 *        "badged" attribute.
    501 * @param onBeforePlacedInWindow (function, optional)
    502 *        Called before the action is placed in the window:
    503 *        onBeforePlacedInWindow(window)
    504 *        * window: The window that the action will be placed in.
    505 * @param onCommand (function, optional)
    506 *        Called when the action is clicked, but only if it has neither a
    507 *        subview nor an iframe:
    508 *        onCommand(event, buttonNode)
    509 *        * event: The triggering event.
    510 *        * buttonNode: The button node that was clicked.
    511 * @param onIframeHiding (function, optional)
    512 *        Called when the action's iframe is hiding:
    513 *        onIframeHiding(iframeNode, parentPanelNode)
    514 *        * iframeNode: The iframe.
    515 *        * parentPanelNode: The panel node in which the iframe is shown.
    516 * @param onIframeHidden (function, optional)
    517 *        Called when the action's iframe is hidden:
    518 *        onIframeHidden(iframeNode, parentPanelNode)
    519 *        * iframeNode: The iframe.
    520 *        * parentPanelNode: The panel node in which the iframe is shown.
    521 * @param onIframeShowing (function, optional)
    522 *        Called when the action's iframe is showing to the user:
    523 *        onIframeShowing(iframeNode, parentPanelNode)
    524 *        * iframeNode: The iframe.
    525 *        * parentPanelNode: The panel node in which the iframe is shown.
    526 * @param onLocationChange (function, optional)
    527 *        Called after tab switch or when the current <browser>'s location
    528 *        changes:
    529 *        onLocationChange(browserWindow)
    530 *        * browserWindow: The browser window containing the tab switch or
    531 *          changed <browser>.
    532 * @param onPlacedInPanel (function, optional)
    533 *        Called when the action is added to the page action panel in a browser
    534 *        window:
    535 *        onPlacedInPanel(buttonNode)
    536 *        * buttonNode: The action's node in the page action panel.
    537 * @param onPlacedInUrlbar (function, optional)
    538 *        Called when the action is added to the urlbar in a browser window:
    539 *        onPlacedInUrlbar(buttonNode)
    540 *        * buttonNode: The action's node in the urlbar.
    541 * @param onRemovedFromWindow (function, optional)
    542 *        Called after the action is removed from a browser window:
    543 *        onRemovedFromWindow(browserWindow)
    544 *        * browserWindow: The browser window that the action was removed from.
    545 * @param onShowingInPanel (function, optional)
    546 *        Called when a browser window's page action panel is showing:
    547 *        onShowingInPanel(buttonNode)
    548 *        * buttonNode: The action's node in the page action panel.
    549 * @param onSubviewPlaced (function, optional)
    550 *        Called when the action's subview is added to its parent panel in a
    551 *        browser window:
    552 *        onSubviewPlaced(panelViewNode)
    553 *        * panelViewNode: The subview's panelview node.
    554 * @param onSubviewShowing (function, optional)
    555 *        Called when the action's subview is showing in a browser window:
    556 *        onSubviewShowing(panelViewNode)
    557 *        * panelViewNode: The subview's panelview node.
    558 * @param pinnedToUrlbar (bool, optional)
    559 *        Pass true to pin the action to the urlbar.  An action is shown in the
    560 *        urlbar if it's pinned and not disabled.  False by default.
    561 * @param tooltip (string, optional)
    562 *        The action's button tooltip text.
    563 * @param urlbarIDOverride (string, optional)
    564 *        Usually the ID of the action's button in the urlbar will be generated
    565 *        automatically.  Pass a string for this property to override that with
    566 *        your own ID.
    567 * @param wantsIframe (bool, optional)
    568 *        Pass true to make an action that shows an iframe in a panel when
    569 *        clicked.
    570 * @param wantsSubview (bool, optional)
    571 *        Pass true to make an action that shows a panel subview when clicked.
    572 * @param disablePrivateBrowsing (bool, optional)
    573 *        Pass true to prevent the action from showing in a private browsing window.
    574 */
    575 function Action(options) {
    576  setProperties(this, options, {
    577    id: true,
    578    title: false,
    579    anchorIDOverride: false,
    580    disabled: false,
    581    extensionID: false,
    582    iconURL: false,
    583    isBadged: false,
    584    labelForHistogram: false,
    585    onBeforePlacedInWindow: false,
    586    onCommand: false,
    587    onIframeHiding: false,
    588    onIframeHidden: false,
    589    onIframeShowing: false,
    590    onLocationChange: false,
    591    onPlacedInPanel: false,
    592    onPlacedInUrlbar: false,
    593    onRemovedFromWindow: false,
    594    onShowingInPanel: false,
    595    onSubviewPlaced: false,
    596    onSubviewShowing: false,
    597    onPinToUrlbarToggled: false,
    598    pinnedToUrlbar: false,
    599    tooltip: false,
    600    urlbarIDOverride: false,
    601    wantsIframe: false,
    602    wantsSubview: false,
    603    disablePrivateBrowsing: false,
    604 
    605    // private
    606 
    607    // (string, optional)
    608    // The ID of another action before which to insert this new action in the
    609    // panel.
    610    _insertBeforeActionID: false,
    611 
    612    // (bool, optional)
    613    // True if this isn't really an action but a separator to be shown in the
    614    // page action panel.
    615    _isSeparator: false,
    616 
    617    // (bool, optional)
    618    // Transient actions have a couple of special properties: (1) They stick to
    619    // the bottom of the panel, and (2) they're hidden in the panel when they're
    620    // disabled.  Other than that they behave like other actions.
    621    _transient: false,
    622 
    623    // (bool, optional)
    624    // True if the action's urlbar button is defined in markup.  In that case, a
    625    // node with the action's urlbar node ID should already exist in the DOM
    626    // (either the auto-generated ID or urlbarIDOverride).  That node will be
    627    // shown when the action is added to the urlbar and hidden when the action
    628    // is removed from the urlbar.
    629    _urlbarNodeInMarkup: false,
    630  });
    631 
    632  /**
    633   * A cache of the pre-computed CSS variable values for a given icon
    634   * URLs object, as passed to _createIconProperties.
    635   */
    636  this._iconProperties = new WeakMap();
    637 
    638  /**
    639   * The global values for the action properties.
    640   */
    641  this._globalProps = {
    642    disabled: this._disabled,
    643    iconURL: this._iconURL,
    644    iconProps: this._createIconProperties(this._iconURL),
    645    title: this._title,
    646    tooltip: this._tooltip,
    647    wantsSubview: this._wantsSubview,
    648  };
    649 
    650  /**
    651   * A mapping of window-specific action property objects, each of which
    652   * derives from the _globalProps object.
    653   */
    654  this._windowProps = new WeakMap();
    655 }
    656 
    657 Action.prototype = {
    658  /**
    659   * The ID of the action's parent extension (string)
    660   */
    661  get extensionID() {
    662    return this._extensionID;
    663  },
    664 
    665  /**
    666   * The action's ID (string)
    667   */
    668  get id() {
    669    return this._id;
    670  },
    671 
    672  get disablePrivateBrowsing() {
    673    return !!this._disablePrivateBrowsing;
    674  },
    675 
    676  /**
    677   * Verifies that the action can be shown in a private window.  For
    678   * extensions, verifies the extension has access to the window.
    679   */
    680  canShowInWindow(browserWindow) {
    681    if (this._extensionID) {
    682      let policy = WebExtensionPolicy.getByID(this._extensionID);
    683      if (!policy.canAccessWindow(browserWindow)) {
    684        return false;
    685      }
    686    }
    687    return !(
    688      this.disablePrivateBrowsing &&
    689      lazy.PrivateBrowsingUtils.isWindowPrivate(browserWindow)
    690    );
    691  },
    692 
    693  /**
    694   * True if the action is pinned to the urlbar.  The action is shown in the
    695   * urlbar if it's pinned and not disabled.  (bool)
    696   */
    697  get pinnedToUrlbar() {
    698    return this._pinnedToUrlbar || false;
    699  },
    700  set pinnedToUrlbar(shown) {
    701    if (this.pinnedToUrlbar != shown) {
    702      this._pinnedToUrlbar = shown;
    703      PageActions.onActionToggledPinnedToUrlbar(this);
    704      this.onPinToUrlbarToggled();
    705    }
    706  },
    707 
    708  /**
    709   * The action's disabled state (bool)
    710   */
    711  getDisabled(browserWindow = null) {
    712    return !!this._getProperties(browserWindow).disabled;
    713  },
    714  setDisabled(value, browserWindow = null) {
    715    return this._setProperty("disabled", !!value, browserWindow);
    716  },
    717 
    718  /**
    719   * The action's icon URL string, or an object mapping sizes to URL strings
    720   * (string or object)
    721   */
    722  getIconURL(browserWindow = null) {
    723    return this._getProperties(browserWindow).iconURL;
    724  },
    725  setIconURL(value, browserWindow = null) {
    726    let props = this._getProperties(browserWindow, !!browserWindow);
    727    props.iconURL = value;
    728    props.iconProps = this._createIconProperties(value);
    729 
    730    this._updateProperty("iconURL", props.iconProps, browserWindow);
    731    return value;
    732  },
    733 
    734  /**
    735   * The set of CSS variables which define the action's icons in various
    736   * sizes. This is generated automatically from the iconURL property.
    737   */
    738  getIconProperties(browserWindow = null) {
    739    return this._getProperties(browserWindow).iconProps;
    740  },
    741 
    742  _createIconProperties(urls) {
    743    if (urls && typeof urls == "object") {
    744      let props = this._iconProperties.get(urls);
    745      if (!props) {
    746        props = Object.freeze({
    747          "--pageAction-image": `image-set(
    748            ${escapeCSSURL(this._iconURLForSize(urls, 16))},
    749            ${escapeCSSURL(this._iconURLForSize(urls, 32))} 2x
    750          )`,
    751        });
    752        this._iconProperties.set(urls, props);
    753      }
    754      return props;
    755    }
    756 
    757    let cssURL = urls ? escapeCSSURL(urls) : null;
    758    return Object.freeze({
    759      "--pageAction-image": cssURL,
    760    });
    761  },
    762 
    763  /**
    764   * The action's title (string). Note, built in actions will
    765   * not have a title property.
    766   */
    767  getTitle(browserWindow = null) {
    768    return this._getProperties(browserWindow).title;
    769  },
    770  setTitle(value, browserWindow = null) {
    771    return this._setProperty("title", value, browserWindow);
    772  },
    773 
    774  /**
    775   * The action's tooltip (string)
    776   */
    777  getTooltip(browserWindow = null) {
    778    return this._getProperties(browserWindow).tooltip;
    779  },
    780  setTooltip(value, browserWindow = null) {
    781    return this._setProperty("tooltip", value, browserWindow);
    782  },
    783 
    784  /**
    785   * Whether the action wants a subview (bool)
    786   */
    787  getWantsSubview(browserWindow = null) {
    788    return !!this._getProperties(browserWindow).wantsSubview;
    789  },
    790  setWantsSubview(value, browserWindow = null) {
    791    return this._setProperty("wantsSubview", !!value, browserWindow);
    792  },
    793 
    794  /**
    795   * Sets a property, optionally for a particular browser window.
    796   *
    797   * @param  name (string, required)
    798   *         The (non-underscored) name of the property.
    799   * @param  value
    800   *         The value.
    801   * @param  browserWindow (DOM window, optional)
    802   *         If given, then the property will be set in this window's state, not
    803   *         globally.
    804   */
    805  _setProperty(name, value, browserWindow) {
    806    let props = this._getProperties(browserWindow, !!browserWindow);
    807    props[name] = value;
    808 
    809    this._updateProperty(name, value, browserWindow);
    810    return value;
    811  },
    812 
    813  _updateProperty(name, value, browserWindow) {
    814    // This may be called before the action has been added.
    815    if (PageActions.actionForID(this.id)) {
    816      for (let bpa of allBrowserPageActions(browserWindow)) {
    817        bpa.updateAction(this, name, { value });
    818      }
    819    }
    820  },
    821 
    822  /**
    823   * Returns the properties object for the given window, if it exists,
    824   * or the global properties object if no window-specific properties
    825   * exist.
    826   *
    827   * @param {Window?} window
    828   *        The window for which to return the properties object, or
    829   *        null to return the global properties object.
    830   * @param {bool} [forceWindowSpecific = false]
    831   *        If true, always returns a window-specific properties object.
    832   *        If a properties object does not exist for the given window,
    833   *        one is created and cached.
    834   * @returns {object}
    835   */
    836  _getProperties(window, forceWindowSpecific = false) {
    837    let props = window && this._windowProps.get(window);
    838 
    839    if (!props && forceWindowSpecific) {
    840      props = Object.create(this._globalProps);
    841      this._windowProps.set(window, props);
    842    }
    843 
    844    return props || this._globalProps;
    845  },
    846 
    847  /**
    848   * Override for the ID of the action's activated-action panel anchor (string)
    849   */
    850  get anchorIDOverride() {
    851    return this._anchorIDOverride;
    852  },
    853 
    854  /**
    855   * Override for the ID of the action's urlbar node (string)
    856   */
    857  get urlbarIDOverride() {
    858    return this._urlbarIDOverride;
    859  },
    860 
    861  /**
    862   * True if the action is shown in an iframe (bool)
    863   */
    864  get wantsIframe() {
    865    return this._wantsIframe || false;
    866  },
    867 
    868  get isBadged() {
    869    return this._isBadged || false;
    870  },
    871 
    872  get labelForHistogram() {
    873    // The histogram label value has a length limit of 20 and restricted to a
    874    // pattern. See MAX_LABEL_LENGTH and CPP_IDENTIFIER_PATTERN in
    875    // toolkit/components/telemetry/parse_histograms.py
    876    return (
    877      this._labelForHistogram ||
    878      this._id.replace(/_\w{1}/g, match => match[1].toUpperCase()).substr(0, 20)
    879    );
    880  },
    881 
    882  /**
    883   * Selects the best matching icon from the given URLs object for the
    884   * given preferred size.
    885   *
    886   * @param {object} urls
    887   *        An object containing square icons of various sizes. The name
    888   *        of each property is its width, and the value is its image URL.
    889   * @param {integer} peferredSize
    890   *        The preferred icon width. The most appropriate icon in the
    891   *        urls object will be chosen to match that size. An exact
    892   *        match will be preferred, followed by an icon exactly double
    893   *        the size, followed by the smallest icon larger than the
    894   *        preferred size, followed by the largest available icon.
    895   * @returns {string}
    896   *        The chosen icon URL.
    897   */
    898  _iconURLForSize(urls, preferredSize) {
    899    // This case is copied from ExtensionParent.sys.mjs so that our image logic is
    900    // the same, so that WebExtensions page action tests that deal with icons
    901    // pass.
    902    let bestSize = null;
    903    if (urls[preferredSize]) {
    904      bestSize = preferredSize;
    905    } else if (urls[2 * preferredSize]) {
    906      bestSize = 2 * preferredSize;
    907    } else {
    908      let sizes = Object.keys(urls)
    909        .map(key => parseInt(key, 10))
    910        .sort((a, b) => a - b);
    911      bestSize =
    912        sizes.find(candidate => candidate > preferredSize) || sizes.pop();
    913    }
    914    return urls[bestSize];
    915  },
    916 
    917  /**
    918   * Performs the command for an action.  If the action has an onCommand
    919   * handler, then it's called.  If the action has a subview or iframe, then a
    920   * panel is opened, displaying the subview or iframe.
    921   *
    922   * @param  browserWindow (DOM window, required)
    923   *         The browser window in which to perform the action.
    924   */
    925  doCommand(browserWindow) {
    926    browserPageActions(browserWindow).doCommandForAction(this);
    927  },
    928 
    929  /**
    930   * Call this when before placing the action in the window.
    931   *
    932   * @param  browserWindow (DOM window, required)
    933   *         The browser window the action will be placed in.
    934   */
    935  onBeforePlacedInWindow(browserWindow) {
    936    if (this._onBeforePlacedInWindow) {
    937      this._onBeforePlacedInWindow(browserWindow);
    938    }
    939  },
    940 
    941  /**
    942   * Call this when the user activates the action.
    943   *
    944   * @param  event (DOM event, required)
    945   *         The triggering event.
    946   * @param  buttonNode (DOM node, required)
    947   *         The action's panel or urlbar button node that was clicked.
    948   */
    949  onCommand(event, buttonNode) {
    950    if (this._onCommand) {
    951      this._onCommand(event, buttonNode);
    952    }
    953  },
    954 
    955  /**
    956   * Call this when the action's iframe is hiding.
    957   *
    958   * @param  iframeNode (DOM node, required)
    959   *         The iframe that's hiding.
    960   * @param  parentPanelNode (DOM node, required)
    961   *         The panel in which the iframe is hiding.
    962   */
    963  onIframeHiding(iframeNode, parentPanelNode) {
    964    if (this._onIframeHiding) {
    965      this._onIframeHiding(iframeNode, parentPanelNode);
    966    }
    967  },
    968 
    969  /**
    970   * Call this when the action's iframe is hidden.
    971   *
    972   * @param  iframeNode (DOM node, required)
    973   *         The iframe that's being hidden.
    974   * @param  parentPanelNode (DOM node, required)
    975   *         The panel in which the iframe is hidden.
    976   */
    977  onIframeHidden(iframeNode, parentPanelNode) {
    978    if (this._onIframeHidden) {
    979      this._onIframeHidden(iframeNode, parentPanelNode);
    980    }
    981  },
    982 
    983  /**
    984   * Call this when the action's iframe is showing.
    985   *
    986   * @param  iframeNode (DOM node, required)
    987   *         The iframe that's being shown.
    988   * @param  parentPanelNode (DOM node, required)
    989   *         The panel in which the iframe is shown.
    990   */
    991  onIframeShowing(iframeNode, parentPanelNode) {
    992    if (this._onIframeShowing) {
    993      this._onIframeShowing(iframeNode, parentPanelNode);
    994    }
    995  },
    996 
    997  /**
    998   * Call this on tab switch or when the current <browser>'s location changes.
    999   *
   1000   * @param  browserWindow (DOM window, required)
   1001   *         The browser window containing the tab switch or changed <browser>.
   1002   */
   1003  onLocationChange(browserWindow) {
   1004    if (this._onLocationChange) {
   1005      this._onLocationChange(browserWindow);
   1006    }
   1007  },
   1008 
   1009  /**
   1010   * Call this when a DOM node for the action is added to the page action panel.
   1011   *
   1012   * @param  buttonNode (DOM node, required)
   1013   *         The action's panel button node.
   1014   */
   1015  onPlacedInPanel(buttonNode) {
   1016    if (this._onPlacedInPanel) {
   1017      this._onPlacedInPanel(buttonNode);
   1018    }
   1019  },
   1020 
   1021  /**
   1022   * Call this when a DOM node for the action is added to the urlbar.
   1023   *
   1024   * @param  buttonNode (DOM node, required)
   1025   *         The action's urlbar button node.
   1026   */
   1027  onPlacedInUrlbar(buttonNode) {
   1028    if (this._onPlacedInUrlbar) {
   1029      this._onPlacedInUrlbar(buttonNode);
   1030    }
   1031  },
   1032 
   1033  /**
   1034   * Call this when the DOM nodes for the action are removed from a browser
   1035   * window.
   1036   *
   1037   * @param  browserWindow (DOM window, required)
   1038   *         The browser window the action was removed from.
   1039   */
   1040  onRemovedFromWindow(browserWindow) {
   1041    if (this._onRemovedFromWindow) {
   1042      this._onRemovedFromWindow(browserWindow);
   1043    }
   1044  },
   1045 
   1046  /**
   1047   * Call this when the action's button is shown in the page action panel.
   1048   *
   1049   * @param  buttonNode (DOM node, required)
   1050   *         The action's panel button node.
   1051   */
   1052  onShowingInPanel(buttonNode) {
   1053    if (this._onShowingInPanel) {
   1054      this._onShowingInPanel(buttonNode);
   1055    }
   1056  },
   1057 
   1058  /**
   1059   * Call this when a panelview node for the action's subview is added to the
   1060   * DOM.
   1061   *
   1062   * @param  panelViewNode (DOM node, required)
   1063   *         The subview's panelview node.
   1064   */
   1065  onSubviewPlaced(panelViewNode) {
   1066    if (this._onSubviewPlaced) {
   1067      this._onSubviewPlaced(panelViewNode);
   1068    }
   1069  },
   1070 
   1071  /**
   1072   * Call this when a panelview node for the action's subview is showing.
   1073   *
   1074   * @param  panelViewNode (DOM node, required)
   1075   *         The subview's panelview node.
   1076   */
   1077  onSubviewShowing(panelViewNode) {
   1078    if (this._onSubviewShowing) {
   1079      this._onSubviewShowing(panelViewNode);
   1080    }
   1081  },
   1082  /**
   1083   * Call this when an icon in the url is pinned or unpinned.
   1084   */
   1085  onPinToUrlbarToggled() {
   1086    if (this._onPinToUrlbarToggled) {
   1087      this._onPinToUrlbarToggled();
   1088    }
   1089  },
   1090 
   1091  /**
   1092   * Removes the action's DOM nodes from all browser windows.
   1093   *
   1094   * PageActions will remember the action's urlbar placement, if any, after this
   1095   * method is called until app shutdown.  If the action is not added again
   1096   * before shutdown, then PageActions will discard the placement, and the next
   1097   * time the action is added, its placement will be reset.
   1098   */
   1099  remove() {
   1100    PageActions.onActionRemoved(this);
   1101  },
   1102 
   1103  /**
   1104   * Returns whether the action should be shown in a given window's panel.
   1105   *
   1106   * @param  browserWindow (DOM window, required)
   1107   *         The window.
   1108   * @return True if the action should be shown and false otherwise.  Actions
   1109   *         are always shown in the panel unless they're both transient and
   1110   *         disabled.
   1111   */
   1112  shouldShowInPanel(browserWindow) {
   1113    // When Proton is enabled, the extension page actions should behave similarly
   1114    // to a transient action, and be hidden from the urlbar overflow menu if they
   1115    // are disabled (as in the urlbar when the overflow menu isn't available)
   1116    //
   1117    // TODO(Bug 1704139): as a follow up we may look into just set on all
   1118    // extensions pageActions `_transient: true`, at least once we sunset
   1119    // the proton preference and we don't need the pre-Proton behavior anymore,
   1120    // and remove this special case.
   1121    const isProtonExtensionAction = this.extensionID;
   1122 
   1123    return (
   1124      (!(this.__transient || isProtonExtensionAction) ||
   1125        !this.getDisabled(browserWindow)) &&
   1126      this.canShowInWindow(browserWindow)
   1127    );
   1128  },
   1129 
   1130  /**
   1131   * Returns whether the action should be shown in a given window's urlbar.
   1132   *
   1133   * @param  browserWindow (DOM window, required)
   1134   *         The window.
   1135   * @return True if the action should be shown and false otherwise.  The action
   1136   *         should be shown if it's both pinned and not disabled.
   1137   */
   1138  shouldShowInUrlbar(browserWindow) {
   1139    return (
   1140      this.pinnedToUrlbar &&
   1141      !this.getDisabled(browserWindow) &&
   1142      this.canShowInWindow(browserWindow)
   1143    );
   1144  },
   1145 
   1146  get _isBuiltIn() {
   1147    let builtInIDs = ["screenshots_mozilla_org"].concat(
   1148      gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id)
   1149    );
   1150    return builtInIDs.includes(this.id);
   1151  },
   1152 
   1153  get _isMozillaAction() {
   1154    return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org";
   1155  },
   1156 };
   1157 
   1158 PageActions.Action = Action;
   1159 
   1160 PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
   1161 PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
   1162 
   1163 // These are only necessary so that the test can use them.
   1164 PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK;
   1165 PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS;
   1166 
   1167 // Sorted in the order in which they should appear in the page action panel.
   1168 // Does not include the page actions of extensions bundled with the browser.
   1169 // They're added by the relevant extension code.
   1170 // NOTE: If you add items to this list (or system add-on actions that we
   1171 // want to keep track of), make sure to also update Histograms.json for the
   1172 // new actions.
   1173 var gBuiltInActions;
   1174 
   1175 PageActions._initBuiltInActions = function () {
   1176  gBuiltInActions = [
   1177    // bookmark
   1178    {
   1179      id: ACTION_ID_BOOKMARK,
   1180      urlbarIDOverride: "star-button-box",
   1181      _urlbarNodeInMarkup: true,
   1182      pinnedToUrlbar: true,
   1183      onShowingInPanel(buttonNode) {
   1184        browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode);
   1185      },
   1186      onCommand(event, buttonNode) {
   1187        browserPageActions(buttonNode).bookmark.onCommand(event);
   1188      },
   1189    },
   1190  ];
   1191 };
   1192 
   1193 /**
   1194 * Gets a BrowserPageActions object in a browser window.
   1195 *
   1196 * @param  obj
   1197 *         Either a DOM node or a browser window.
   1198 * @return The BrowserPageActions object in the browser window related to the
   1199 *         given object.
   1200 */
   1201 function browserPageActions(obj) {
   1202  if (obj.BrowserPageActions) {
   1203    return obj.BrowserPageActions;
   1204  }
   1205  return obj.ownerGlobal.BrowserPageActions;
   1206 }
   1207 
   1208 /**
   1209 * A generator function for all open browser windows.
   1210 *
   1211 * @param browserWindow (DOM window, optional)
   1212 *        If given, then only this window will be yielded.  That may sound
   1213 *        pointless, but it can make callers nicer to write since they don't
   1214 *        need two separate cases, one where a window is given and another where
   1215 *        it isn't.
   1216 */
   1217 function* allBrowserWindows(browserWindow = null) {
   1218  if (browserWindow) {
   1219    yield browserWindow;
   1220    return;
   1221  }
   1222  yield* Services.wm.getEnumerator("navigator:browser");
   1223 }
   1224 
   1225 /**
   1226 * A generator function for BrowserPageActions objects in all open windows.
   1227 *
   1228 * @param browserWindow (DOM window, optional)
   1229 *        If given, then the BrowserPageActions for only this window will be
   1230 *        yielded.
   1231 */
   1232 function* allBrowserPageActions(browserWindow = null) {
   1233  for (let win of allBrowserWindows(browserWindow)) {
   1234    yield browserPageActions(win);
   1235  }
   1236 }
   1237 
   1238 /**
   1239 * A simple function that sets properties on a given object while doing basic
   1240 * required-properties checking.  If a required property isn't specified in the
   1241 * given options object, or if the options object has properties that aren't in
   1242 * the given schema, then an error is thrown.
   1243 *
   1244 * @param  obj
   1245 *         The object to set properties on.
   1246 * @param  options
   1247 *         An options object supplied by the consumer.
   1248 * @param  schema
   1249 *         An object a property for each required and optional property.  The
   1250 *         keys are property names; the value of a key is a bool that is true if
   1251 *         the property is required.
   1252 */
   1253 function setProperties(obj, options, schema) {
   1254  for (let name in schema) {
   1255    let required = schema[name];
   1256    if (required && !(name in options)) {
   1257      throw new Error(`'${name}' must be specified`);
   1258    }
   1259    let nameInObj = "_" + name;
   1260    if (name[0] == "_") {
   1261      // The property is "private".  If it's defined in the options, then define
   1262      // it on obj exactly as it's defined on options.
   1263      if (name in options) {
   1264        obj[nameInObj] = options[name];
   1265      }
   1266    } else {
   1267      // The property is "public".  Make sure the property is defined on obj.
   1268      obj[nameInObj] = options[name] || null;
   1269    }
   1270  }
   1271  for (let name in options) {
   1272    if (!(name in schema)) {
   1273      throw new Error(`Unrecognized option '${name}'`);
   1274    }
   1275  }
   1276 }