Bug 1871378 - update Fetch algorithm to restrict Fetch body Size for keepalive reques...
[gecko.git] / browser / modules / PageActions.sys.mjs
blob2a900c21f0a6542af577d8bdfac6332772919ffa
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/. */
5 const lazy = {};
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 });
15 const ACTION_ID_BOOKMARK = "bookmark";
16 const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
17 const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
19 const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
20 const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
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)}")`;
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();
39     let callbacks = this._deferredAddActionCalls;
40     delete this._deferredAddActionCalls;
42     this._loadPersistedActions();
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     }
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     }
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     }
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   },
79   _deferredAddActionCalls: [],
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   },
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   },
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   },
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   },
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   },
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);
204     // Insert the action into the appropriate list, either _builtInActions or
205     // _nonBuiltInActions.
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     }
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     }
250     // Actions are always pinned to the urlbar, except for panel separators.
251     action._pinnedToUrlbar = !action.__isSeparator;
252     this._updateIDsPinnedToUrlbarForAction(action);
253   },
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   },
274   // These keep track of currently registered actions.
275   _builtInActions: [],
276   _nonBuiltInActions: [],
277   _transientActions: [],
278   _actionsByID: new Map(),
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     }
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     }
306     for (let bpa of allBrowserPageActions()) {
307       bpa.removeAction(action);
308     }
309   },
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   },
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   },
337   _storePersistedActions() {
338     let json = JSON.stringify(this._persistedActions);
339     Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
340   },
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) {}
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) {}
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   },
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   },
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   },
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   },
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   },
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.initialized;
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   },
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   },
466  * A single page action.
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.
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.
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,
605     // private
607     // (string, optional)
608     // The ID of another action before which to insert this new action in the
609     // panel.
610     _insertBeforeActionID: false,
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,
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,
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   });
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();
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   };
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();
657 Action.prototype = {
658   /**
659    * The ID of the action's parent extension (string)
660    */
661   get extensionID() {
662     return this._extensionID;
663   },
665   /**
666    * The action's ID (string)
667    */
668   get id() {
669     return this._id;
670   },
672   get disablePrivateBrowsing() {
673     return !!this._disablePrivateBrowsing;
674   },
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   },
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   },
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   },
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);
730     this._updateProperty("iconURL", props.iconProps, browserWindow);
731     return value;
732   },
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   },
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     }
757     let cssURL = urls ? escapeCSSURL(urls) : null;
758     return Object.freeze({
759       "--pageAction-image": cssURL,
760     });
761   },
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   },
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   },
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   },
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;
809     this._updateProperty(name, value, browserWindow);
810     return value;
811   },
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   },
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);
839     if (!props && forceWindowSpecific) {
840       props = Object.create(this._globalProps);
841       this._windowProps.set(window, props);
842     }
844     return props || this._globalProps;
845   },
847   /**
848    * Override for the ID of the action's activated-action panel anchor (string)
849    */
850   get anchorIDOverride() {
851     return this._anchorIDOverride;
852   },
854   /**
855    * Override for the ID of the action's urlbar node (string)
856    */
857   get urlbarIDOverride() {
858     return this._urlbarIDOverride;
859   },
861   /**
862    * True if the action is shown in an iframe (bool)
863    */
864   get wantsIframe() {
865     return this._wantsIframe || false;
866   },
868   get isBadged() {
869     return this._isBadged || false;
870   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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   },
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;
1123     return (
1124       (!(this.__transient || isProtonExtensionAction) ||
1125         !this.getDisabled(browserWindow)) &&
1126       this.canShowInWindow(browserWindow)
1127     );
1128   },
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   },
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   },
1153   get _isMozillaAction() {
1154     return this._isBuiltIn || this.id == "webcompat-reporter_mozilla_org";
1155   },
1158 PageActions.Action = Action;
1160 PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
1161 PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
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;
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;
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   ];
1194  * Gets a BrowserPageActions object in a browser window.
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;
1209  * A generator function for all open browser windows.
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");
1226  * A generator function for BrowserPageActions objects in all open windows.
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   }
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.
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   }