tor-browser

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

SharingUtils.sys.mjs (9484B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      7 import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs";
      8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      9 
     10 const APPLE_COPY_LINK = "com.apple.share.CopyLink.invite";
     11 
     12 let lazy = {};
     13 
     14 XPCOMUtils.defineLazyServiceGetters(lazy, {
     15  MacSharingService: [
     16    "@mozilla.org/widget/macsharingservice;1",
     17    Ci.nsIMacSharingService,
     18  ],
     19  WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils],
     20 });
     21 
     22 class SharingUtilsCls {
     23  /**
     24   * Updates a sharing item in a given menu, creating it if necessary.
     25   */
     26  updateShareURLMenuItem(browser, insertAfterEl) {
     27    if (!Services.prefs.getBoolPref("browser.menu.share_url.allow", true)) {
     28      return;
     29    }
     30 
     31    let shareURL = insertAfterEl.nextElementSibling;
     32    if (!shareURL?.matches(".share-tab-url-item")) {
     33      shareURL = this.#createShareURLMenuItem(insertAfterEl);
     34    }
     35 
     36    shareURL.browserToShare = Cu.getWeakReference(browser);
     37    if (AppConstants.platform != "macosx") {
     38      // On macOS, we keep the item enabled and handle enabled state
     39      // inside the menupopup.
     40      // Everywhere else, we disable the item, as there's no submenu.
     41      shareURL.hidden = !BrowserUtils.getShareableURL(browser.currentURI);
     42    }
     43  }
     44 
     45  /**
     46   * Creates and returns the "Share" menu item.
     47   */
     48  #createShareURLMenuItem(insertAfterEl) {
     49    let menu = insertAfterEl.parentNode;
     50    let shareURL = null;
     51    let document = insertAfterEl.ownerDocument;
     52    if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
     53      shareURL = this.#buildCopyLinkItem(document);
     54    } else {
     55      if (AppConstants.platform == "win") {
     56        shareURL = this.#buildShareURLItem(document);
     57      } else if (AppConstants.platform == "macosx") {
     58        shareURL = this.#buildShareURLMenu(document);
     59      }
     60      let l10nID =
     61        menu.id == "tabContextMenu"
     62          ? "tab-context-share-url"
     63          : "menu-file-share-url";
     64      document.l10n.setAttributes(shareURL, l10nID);
     65    }
     66    shareURL.classList.add("share-tab-url-item");
     67 
     68    menu.insertBefore(shareURL, insertAfterEl.nextSibling);
     69    return shareURL;
     70  }
     71 
     72  /**
     73   * Returns a menu item specifically for accessing Windows sharing services.
     74   */
     75  #buildShareURLItem(document) {
     76    let shareURLMenuItem = document.createXULElement("menuitem");
     77    shareURLMenuItem.addEventListener("command", this);
     78    return shareURLMenuItem;
     79  }
     80 
     81  /**
     82   * Returns a menu specifically for accessing macOSx sharing services .
     83   */
     84  #buildShareURLMenu(document) {
     85    let menu = document.createXULElement("menu");
     86    let menuPopup = document.createXULElement("menupopup");
     87    menuPopup.addEventListener("popupshowing", this);
     88    menu.appendChild(menuPopup);
     89    return menu;
     90  }
     91 
     92  /**
     93   * Return a menuitem that only copies the link. Useful for
     94   * OSes where we do not yet have full share support, like Linux.
     95   *
     96   * We currently also use this on macOS because for some reason Apple does not
     97   * provide the share service option for this.
     98   */
     99  #buildCopyLinkItem(document) {
    100    let shareURLMenuItem = document.createXULElement("menuitem");
    101    document.l10n.setAttributes(shareURLMenuItem, "menu-share-copy-link");
    102    shareURLMenuItem.classList.add("share-copy-link");
    103 
    104    if (AppConstants.platform == "macosx") {
    105      shareURLMenuItem.classList.add("menuitem-iconic");
    106      shareURLMenuItem.setAttribute(
    107        "image",
    108        "chrome://global/skin/icons/link.svg"
    109      );
    110    } else {
    111      // On macOS the command handling happens by virtue of the submenu
    112      // command event listener.
    113      shareURLMenuItem.addEventListener("command", this);
    114    }
    115    return shareURLMenuItem;
    116  }
    117 
    118  /**
    119   * Get the sharing data for a given DOM node.
    120   */
    121  getDataToShare(node) {
    122    let browser = node.browserToShare?.get();
    123    let urlToShare = null;
    124    let titleToShare = null;
    125 
    126    if (browser) {
    127      let maybeToShare = BrowserUtils.getShareableURL(browser.currentURI);
    128      if (maybeToShare) {
    129        urlToShare = maybeToShare;
    130        titleToShare = browser.contentTitle;
    131      }
    132    }
    133    return { urlToShare, titleToShare };
    134  }
    135 
    136  /**
    137   * Populates the "Share" menupopup on macOSx.
    138   */
    139  initializeShareURLPopup(menuPopup) {
    140    if (AppConstants.platform != "macosx") {
    141      return;
    142    }
    143 
    144    // Empty menupopup
    145    while (menuPopup.firstChild) {
    146      menuPopup.firstChild.remove();
    147    }
    148 
    149    let document = menuPopup.ownerDocument;
    150    let { gURLBar } = menuPopup.ownerGlobal;
    151 
    152    let { urlToShare } = this.getDataToShare(menuPopup.parentNode);
    153 
    154    // If we can't share the current URL, we display the items disabled,
    155    // but enable the "more..." item at the bottom, to allow the user to
    156    // change sharing preferences in the system dialog.
    157    let shouldEnable = !!urlToShare;
    158    if (!urlToShare) {
    159      // Fake it so we can ask the sharing service for services:
    160      urlToShare = Services.io.newURI("https://mozilla.org/");
    161    }
    162 
    163    let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec;
    164    let services = lazy.MacSharingService.getSharingProviders(currentURI);
    165 
    166    // Apple seems reluctant to provide copy link as a feature. Add it at the
    167    // start if it's not there.
    168    if (!services.some(s => s.name == APPLE_COPY_LINK)) {
    169      let item = this.#buildCopyLinkItem(document);
    170      if (!shouldEnable) {
    171        item.setAttribute("disabled", "true");
    172      }
    173      menuPopup.appendChild(item);
    174    }
    175 
    176    services.forEach(share => {
    177      let item = document.createXULElement("menuitem");
    178      item.classList.add("menuitem-iconic");
    179      item.setAttribute("label", share.menuItemTitle);
    180      item.setAttribute("share-name", share.name);
    181      item.setAttribute("image", ChromeUtils.encodeURIForSrcset(share.image));
    182      if (!shouldEnable) {
    183        item.setAttribute("disabled", "true");
    184      }
    185      menuPopup.appendChild(item);
    186    });
    187    menuPopup.appendChild(document.createXULElement("menuseparator"));
    188    let moreItem = document.createXULElement("menuitem");
    189    document.l10n.setAttributes(moreItem, "menu-share-more");
    190    moreItem.classList.add("menuitem-iconic", "share-more-button");
    191    menuPopup.appendChild(moreItem);
    192 
    193    menuPopup.addEventListener("command", this);
    194    menuPopup.parentNode
    195      .closest("menupopup")
    196      .addEventListener("popuphiding", this);
    197    menuPopup.setAttribute("data-initialized", true);
    198  }
    199 
    200  onShareURLCommand(event) {
    201    // Only call sharing services for the "Share" menu item. These services
    202    // are accessed from a submenu popup for MacOS or the "Share" menu item
    203    // for Windows. Use .closest() as a hack to find either the item itself
    204    // or a parent with the right class.
    205    let target = event.target.closest(".share-tab-url-item");
    206    if (!target) {
    207      return;
    208    }
    209    let { gURLBar } = target.ownerGlobal;
    210 
    211    // urlToShare/titleToShare may be null, in which case only the "more"
    212    // item is enabled, so handle that case first:
    213    if (event.target.classList.contains("share-more-button")) {
    214      lazy.MacSharingService.openSharingPreferences();
    215      return;
    216    }
    217 
    218    let { urlToShare, titleToShare } = this.getDataToShare(target);
    219    let currentURI = gURLBar.makeURIReadable(urlToShare).displaySpec;
    220 
    221    if (event.target.classList.contains("share-copy-link")) {
    222      BrowserUtils.copyLink(currentURI, titleToShare);
    223    } else if (AppConstants.platform == "win") {
    224      lazy.WindowsUIUtils.shareUrl(currentURI, titleToShare);
    225    } else {
    226      // On macOSX platforms
    227      let shareName = event.target.getAttribute("share-name");
    228      if (shareName) {
    229        lazy.MacSharingService.shareUrl(shareName, currentURI, titleToShare);
    230      }
    231    }
    232  }
    233 
    234  onPopupHiding(event) {
    235    // We don't want to rebuild the contents of the "Share" menupopup if only its submenu is
    236    // hidden. So bail if this isn't the top menupopup in the DOM tree:
    237    if (event.target.parentNode.closest("menupopup")) {
    238      return;
    239    }
    240    // Otherwise, clear its "data-initialized" attribute.
    241    let menupopup = event.target.querySelector(
    242      ".share-tab-url-item"
    243    )?.menupopup;
    244    menupopup?.removeAttribute("data-initialized");
    245 
    246    event.target.removeEventListener("popuphiding", this);
    247  }
    248 
    249  onPopupShowing(event) {
    250    if (!event.target.hasAttribute("data-initialized")) {
    251      this.initializeShareURLPopup(event.target);
    252    }
    253  }
    254 
    255  handleEvent(aEvent) {
    256    switch (aEvent.type) {
    257      case "command":
    258        this.onShareURLCommand(aEvent);
    259        break;
    260      case "popuphiding":
    261        this.onPopupHiding(aEvent);
    262        break;
    263      case "popupshowing":
    264        this.onPopupShowing(aEvent);
    265        break;
    266    }
    267  }
    268 
    269  testOnlyMockUIUtils(mock) {
    270    if (!Cu.isInAutomation) {
    271      throw new Error("Can only mock utils in automation.");
    272    }
    273    // eslint-disable-next-line mozilla/valid-lazy
    274    Object.defineProperty(lazy, "WindowsUIUtils", {
    275      get() {
    276        if (mock) {
    277          return mock;
    278        }
    279        return Cc["@mozilla.org/windows-ui-utils;1"].getService(
    280          Ci.nsIWindowsUIUtils
    281        );
    282      },
    283    });
    284  }
    285 }
    286 
    287 export let SharingUtils = new SharingUtilsCls();