tor-browser

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

ext-sidebarAction.js (14733B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 "use strict";
      8 
      9 var { ExtensionParent } = ChromeUtils.importESModule(
     10  "resource://gre/modules/ExtensionParent.sys.mjs"
     11 );
     12 var { ExtensionError } = ExtensionUtils;
     13 
     14 var { IconDetails } = ExtensionParent;
     15 
     16 ChromeUtils.defineESModuleGetters(this, {
     17  SidebarManager:
     18    "moz-src:///browser/components/sidebar/SidebarManager.sys.mjs",
     19 });
     20 
     21 // WeakMap[Extension -> SidebarAction]
     22 let sidebarActionMap = new WeakMap();
     23 
     24 /**
     25 * Responsible for the sidebar_action section of the manifest as well
     26 * as the associated sidebar browser.
     27 */
     28 this.sidebarAction = class extends ExtensionAPI {
     29  static for(extension) {
     30    return sidebarActionMap.get(extension);
     31  }
     32 
     33  onManifestEntry() {
     34    let { extension } = this;
     35 
     36    extension.once("ready", this.onReady.bind(this));
     37 
     38    let options = extension.manifest.sidebar_action;
     39 
     40    // Add the extension to the sidebar menu.  The sidebar widget will copy
     41    // from that when it is viewed, so we shouldn't need to update that.
     42    let widgetId = makeWidgetId(extension.id);
     43    this.id = `${widgetId}-sidebar-action`;
     44    this.menuId = `menubar_menu_${this.id}`;
     45 
     46    this.browserStyle = options.browser_style;
     47 
     48    this.defaults = {
     49      enabled: true,
     50      title: options.default_title || extension.name,
     51      icon: IconDetails.normalize({ path: options.default_icon }, extension),
     52      panel: options.default_panel || "",
     53    };
     54    this.globals = Object.create(this.defaults);
     55 
     56    this.tabContext = new TabContext(target => {
     57      let window = target.ownerGlobal;
     58      if (target === window) {
     59        return this.globals;
     60      }
     61      return this.tabContext.get(window);
     62    });
     63 
     64    // We need to ensure our elements are available before session restore.
     65    this.windowOpenListener = window => {
     66      this.createMenuItem(window, this.globals);
     67    };
     68    windowTracker.addOpenListener(this.windowOpenListener);
     69 
     70    sidebarActionMap.set(extension, this);
     71  }
     72 
     73  onReady() {
     74    this.build();
     75  }
     76 
     77  /**
     78   * Called by any extension when any of the following happens:
     79   * - An extension has an update including whether it is a sidebar
     80   * - An extension is disabled or removed
     81   * - On browser shutdown
     82   *
     83   * @param {boolean} isAppShutdown
     84   *        Whether this is called during app shutdown
     85   */
     86  onShutdown(isAppShutdown) {
     87    if (!sidebarActionMap.delete(this.extension)) {
     88      // sidebar_action not specified for this extension.
     89      return;
     90    }
     91    this.tabContext.shutdown();
     92    // Don't remove everything on app shutdown so session restore can handle
     93    // restoring open sidebars.
     94    if (isAppShutdown) {
     95      return;
     96    }
     97 
     98    for (let window of windowTracker.browserWindows()) {
     99      let { SidebarController } = window;
    100      // Note: sidebar preferences such as sidebar.installed.extensions are kept to remember users preferences
    101      // and should be remembered between browser/extension restarts, when the extension is disabled and re-enabled,
    102      // and across updates (including updates that drop sidebar_action). We should only forget about these on uninstall.
    103      SidebarController.removeExtension(this.id);
    104    }
    105    windowTracker.removeOpenListener(this.windowOpenListener);
    106  }
    107 
    108  static onUninstall(id) {
    109    const sidebarId = `${makeWidgetId(id)}-sidebar-action`;
    110 
    111    let installedExtensions = Services.prefs
    112      .getStringPref("sidebar.installed.extensions", "")
    113      .split(",");
    114    const index = installedExtensions.indexOf(id);
    115    if (index != -1) {
    116      SidebarManager.cleanupPrefs(id);
    117    }
    118 
    119    for (let window of windowTracker.browserWindows()) {
    120      let { SidebarController } = window;
    121      if (SidebarController.lastOpenedId === sidebarId) {
    122        SidebarController.lastOpenedId = null;
    123      }
    124    }
    125  }
    126 
    127  build() {
    128    // eslint-disable-next-line mozilla/balanced-listeners
    129    this.tabContext.on("tab-select", (evt, tab) => {
    130      this.updateWindow(tab.ownerGlobal);
    131    });
    132 
    133    let install = this.extension.startupReason === "ADDON_INSTALL";
    134    for (let window of windowTracker.browserWindows()) {
    135      this.updateWindow(window);
    136      let { SidebarController } = window;
    137      if (
    138        (install || SidebarController.lastOpenedId == this.id) &&
    139        this.extension.manifest.sidebar_action.open_at_install
    140      ) {
    141        SidebarController.show(this.id);
    142      }
    143    }
    144  }
    145 
    146  createMenuItem(window, details) {
    147    if (!this.extension.canAccessWindow(window)) {
    148      return;
    149    }
    150    this.panel = details.panel;
    151    let { SidebarController, devicePixelRatio } = window;
    152    SidebarController.registerExtension(this.id, {
    153      ...this.getMenuIcon(details, devicePixelRatio),
    154      menuId: this.menuId,
    155      title: details.title,
    156      extensionId: this.extension.id,
    157      onload: () =>
    158        SidebarController.browser.contentWindow.loadPanel(
    159          this.extension.id,
    160          this.panel,
    161          this.browserStyle
    162        ),
    163    });
    164  }
    165 
    166  /**
    167   * Retrieve the icon to be rendered in sidebar menus.
    168   *
    169   * @param {object} details
    170   * @param {object} details.icon
    171   *   Extension icons.
    172   * @param {number} scale
    173   *   Scaling factor of the icon's size.
    174   * @returns {{ icon: string; iconUrl: string }}
    175   */
    176  getMenuIcon({ icon }, scale) {
    177    let getIcon = size =>
    178      IconDetails.escapeUrl(
    179        IconDetails.getPreferredIcon(icon, this.extension, size).icon
    180      );
    181 
    182    const iconUrl = getIcon(16 * scale);
    183    // TODO Bug 1898257 - Only return iconUrl here, remove usages of icon.
    184    return {
    185      icon: `image-set(url("${getIcon(16)}"), url("${getIcon(32)}") 2x)`,
    186      iconUrl,
    187    };
    188  }
    189 
    190  /**
    191   * Update the menu items with the tab context data in `tabData`.
    192   *
    193   * @param {ChromeWindow} window
    194   *        Browser chrome window.
    195   * @param {object} tabData
    196   *        Tab specific sidebar configuration.
    197   */
    198  updateButton(window, tabData) {
    199    let { document, SidebarController, devicePixelRatio } = window;
    200    let title = tabData.title || this.extension.name;
    201    if (!document.getElementById(this.menuId)) {
    202      // Menu items are added when new windows are opened, or from onReady (when
    203      // an extension has fully started). The menu item may be missing at this
    204      // point if the extension updates the sidebar during its startup.
    205      this.createMenuItem(window, tabData);
    206    }
    207    let urlChanged = tabData.panel !== this.panel;
    208    if (urlChanged) {
    209      this.panel = tabData.panel;
    210    }
    211    SidebarController.setExtensionAttributes(
    212      this.id,
    213      {
    214        ...this.getMenuIcon(tabData, devicePixelRatio),
    215        label: title,
    216      },
    217      urlChanged
    218    );
    219  }
    220 
    221  /**
    222   * Update the menu items for a given window.
    223   *
    224   * @param {ChromeWindow} window
    225   *        Browser chrome window.
    226   */
    227  updateWindow(window) {
    228    if (!this.extension.canAccessWindow(window)) {
    229      return;
    230    }
    231    let nativeTab = window.gBrowser.selectedTab;
    232    this.updateButton(window, this.tabContext.get(nativeTab));
    233  }
    234 
    235  /**
    236   * Update the menu items when the extension changes the icon,
    237   * title, url, etc. If it only changes a parameter for a single tab, `target`
    238   * will be that tab. If it only changes a parameter for a single window,
    239   * `target` will be that window. Otherwise `target` will be null.
    240   *
    241   * @param {XULElement|ChromeWindow|null} target
    242   *        Browser tab or browser chrome window, may be null.
    243   */
    244  updateOnChange(target) {
    245    if (target) {
    246      let window = target.ownerGlobal;
    247      if (target === window || target.selected) {
    248        this.updateWindow(window);
    249      }
    250    } else {
    251      for (let window of windowTracker.browserWindows()) {
    252        this.updateWindow(window);
    253      }
    254    }
    255  }
    256 
    257  /**
    258   * Gets the target object corresponding to the `details` parameter of the various
    259   * get* and set* API methods.
    260   *
    261   * @param {object} details
    262   *        An object with optional `tabId` or `windowId` properties.
    263   * @param {number} [details.tabId]
    264   *        The target tab.
    265   * @param {number} [details.windowId]
    266   *        The target window.
    267   * @throws if both `tabId` and `windowId` are specified, or if they are invalid.
    268   * @returns {XULElement|ChromeWindow|null}
    269   *        If a `tabId` was specified, the corresponding XULElement tab.
    270   *        If a `windowId` was specified, the corresponding ChromeWindow.
    271   *        Otherwise, `null`.
    272   */
    273  getTargetFromDetails({ tabId, windowId }) {
    274    if (tabId != null && windowId != null) {
    275      throw new ExtensionError(
    276        "Only one of tabId and windowId can be specified."
    277      );
    278    }
    279    let target = null;
    280    if (tabId != null) {
    281      target = tabTracker.getTab(tabId);
    282      if (!this.extension.canAccessWindow(target.ownerGlobal)) {
    283        throw new ExtensionError(`Invalid tab ID: ${tabId}`);
    284      }
    285    } else if (windowId != null) {
    286      target = windowTracker.getWindow(windowId);
    287      if (!this.extension.canAccessWindow(target)) {
    288        throw new ExtensionError(`Invalid window ID: ${windowId}`);
    289      }
    290    }
    291    return target;
    292  }
    293 
    294  /**
    295   * Gets the data associated with a tab, window, or the global one.
    296   *
    297   * @param {XULElement|ChromeWindow|null} target
    298   *        A XULElement tab, a ChromeWindow, or null for the global data.
    299   * @returns {object}
    300   *        The icon, title, panel, etc. associated with the target.
    301   */
    302  getContextData(target) {
    303    if (target) {
    304      return this.tabContext.get(target);
    305    }
    306    return this.globals;
    307  }
    308 
    309  /**
    310   * Set a global, window specific or tab specific property.
    311   *
    312   * @param {XULElement|ChromeWindow|null} target
    313   *        A XULElement tab, a ChromeWindow, or null for the global data.
    314   * @param {string} prop
    315   *        String property to set ["icon", "title", or "panel"].
    316   * @param {string} value
    317   *        Value for property.
    318   */
    319  setProperty(target, prop, value) {
    320    let values = this.getContextData(target);
    321    if (value === null) {
    322      delete values[prop];
    323    } else {
    324      values[prop] = value;
    325    }
    326 
    327    this.updateOnChange(target);
    328  }
    329 
    330  /**
    331   * Retrieve the value of a global, window specific or tab specific property.
    332   *
    333   * @param {XULElement|ChromeWindow|null} target
    334   *        A XULElement tab, a ChromeWindow, or null for the global data.
    335   * @param {string} prop
    336   *        String property to retrieve ["icon", "title", or "panel"]
    337   * @returns {string} value
    338   *          Value of prop.
    339   */
    340  getProperty(target, prop) {
    341    return this.getContextData(target)[prop];
    342  }
    343 
    344  setPropertyFromDetails(details, prop, value) {
    345    return this.setProperty(this.getTargetFromDetails(details), prop, value);
    346  }
    347 
    348  getPropertyFromDetails(details, prop) {
    349    return this.getProperty(this.getTargetFromDetails(details), prop);
    350  }
    351 
    352  /**
    353   * Triggers this sidebar action for the given window, with the same effects as
    354   * if it were toggled via menu or toolbarbutton by a user.
    355   *
    356   * @param {ChromeWindow} window
    357   */
    358  triggerAction(window) {
    359    let { SidebarController } = window;
    360    if (SidebarController && this.extension.canAccessWindow(window)) {
    361      SidebarController.toggle(this.id);
    362    }
    363  }
    364 
    365  /**
    366   * Opens this sidebar action for the given window.
    367   *
    368   * @param {ChromeWindow} window
    369   */
    370  open(window) {
    371    let { SidebarController } = window;
    372    if (SidebarController && this.extension.canAccessWindow(window)) {
    373      SidebarController.show(this.id);
    374    }
    375  }
    376 
    377  /**
    378   * Closes this sidebar action for the given window if this sidebar action is open.
    379   *
    380   * @param {ChromeWindow} window
    381   */
    382  close(window) {
    383    if (this.isOpen(window)) {
    384      window.SidebarController.hide();
    385    }
    386  }
    387 
    388  /**
    389   * Toogles this sidebar action for the given window
    390   *
    391   * @param {ChromeWindow} window
    392   */
    393  toggle(window) {
    394    let { SidebarController } = window;
    395    if (!SidebarController || !this.extension.canAccessWindow(window)) {
    396      return;
    397    }
    398 
    399    if (!this.isOpen(window)) {
    400      SidebarController.show(this.id);
    401    } else {
    402      SidebarController.hide();
    403    }
    404  }
    405 
    406  /**
    407   * Checks whether this sidebar action is open in the given window.
    408   *
    409   * @param {ChromeWindow} window
    410   * @returns {boolean}
    411   */
    412  isOpen(window) {
    413    let { SidebarController } = window;
    414    return SidebarController.isOpen && this.id == SidebarController.currentID;
    415  }
    416 
    417  getAPI(context) {
    418    let { extension } = context;
    419    const sidebarAction = this;
    420 
    421    return {
    422      sidebarAction: {
    423        async setTitle(details) {
    424          sidebarAction.setPropertyFromDetails(details, "title", details.title);
    425        },
    426 
    427        getTitle(details) {
    428          return sidebarAction.getPropertyFromDetails(details, "title");
    429        },
    430 
    431        async setIcon(details) {
    432          let icon = IconDetails.normalize(details, extension, context);
    433          if (!Object.keys(icon).length) {
    434            icon = null;
    435          }
    436          sidebarAction.setPropertyFromDetails(details, "icon", icon);
    437        },
    438 
    439        async setPanel(details) {
    440          let url;
    441          // Clear the url when given null or empty string.
    442          if (!details.panel) {
    443            url = null;
    444          } else {
    445            url = context.uri.resolve(details.panel);
    446            if (!context.checkLoadURL(url)) {
    447              return Promise.reject({
    448                message: `Access denied for URL ${url}`,
    449              });
    450            }
    451          }
    452 
    453          sidebarAction.setPropertyFromDetails(details, "panel", url);
    454        },
    455 
    456        getPanel(details) {
    457          return sidebarAction.getPropertyFromDetails(details, "panel");
    458        },
    459 
    460        open() {
    461          let window = windowTracker.topWindow;
    462          if (context.canAccessWindow(window)) {
    463            sidebarAction.open(window);
    464          }
    465        },
    466 
    467        close() {
    468          let window = windowTracker.topWindow;
    469          if (context.canAccessWindow(window)) {
    470            sidebarAction.close(window);
    471          }
    472        },
    473 
    474        toggle() {
    475          let window = windowTracker.topWindow;
    476          if (context.canAccessWindow(window)) {
    477            sidebarAction.toggle(window);
    478          }
    479        },
    480 
    481        isOpen(details) {
    482          let { windowId } = details;
    483          if (windowId == null) {
    484            windowId = Window.WINDOW_ID_CURRENT;
    485          }
    486          let window = windowTracker.getWindow(windowId, context);
    487          return sidebarAction.isOpen(window);
    488        },
    489      },
    490    };
    491  }
    492 };
    493 
    494 global.sidebarActionFor = this.sidebarAction.for;