tor-browser

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

ext-android.js (16165B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 /**
      7 * NOTE: If you change the globals in this file, you must check if the globals
      8 * list in mobile/android/.eslintrc.js also needs updating.
      9 */
     10 
     11 ChromeUtils.defineESModuleGetters(this, {
     12  GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs",
     13  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     14  mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
     15 });
     16 
     17 var { EventDispatcher } = ChromeUtils.importESModule(
     18  "resource://gre/modules/Messaging.sys.mjs"
     19 );
     20 
     21 var { ExtensionCommon } = ChromeUtils.importESModule(
     22  "resource://gre/modules/ExtensionCommon.sys.mjs"
     23 );
     24 var { ExtensionUtils } = ChromeUtils.importESModule(
     25  "resource://gre/modules/ExtensionUtils.sys.mjs"
     26 );
     27 
     28 var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
     29 
     30 var { defineLazyGetter } = ExtensionCommon;
     31 
     32 const BrowserStatusFilter = Components.Constructor(
     33  "@mozilla.org/appshell/component/browser-status-filter;1",
     34  "nsIWebProgress",
     35  "addProgressListener"
     36 );
     37 
     38 const WINDOW_TYPE = "navigator:geckoview";
     39 
     40 // We need let to break cyclic dependency
     41 /* eslint-disable-next-line prefer-const */
     42 let windowTracker;
     43 
     44 /**
     45 * A nsIWebProgressListener for a specific XUL browser, which delegates the
     46 * events that it receives to a tab progress listener, and prepends the browser
     47 * to their arguments list.
     48 *
     49 * @param {XULElement} browser
     50 *        A XUL browser element.
     51 * @param {object} listener
     52 *        A tab progress listener object.
     53 * @param {integer} flags
     54 *        The web progress notification flags with which to filter events.
     55 */
     56 class BrowserProgressListener {
     57  constructor(browser, listener, flags) {
     58    this.listener = listener;
     59    this.browser = browser;
     60    this.filter = new BrowserStatusFilter(this, flags);
     61    this.browser.addProgressListener(this.filter, flags);
     62  }
     63 
     64  /**
     65   * Destroy the listener, and perform any necessary cleanup.
     66   */
     67  destroy() {
     68    this.browser.removeProgressListener(this.filter);
     69    this.filter.removeProgressListener(this);
     70  }
     71 
     72  /**
     73   * Calls the appropriate listener in the wrapped tab progress listener, with
     74   * the wrapped XUL browser object as its first argument, and the additional
     75   * arguments in `args`.
     76   *
     77   * @param {string} method
     78   *        The name of the nsIWebProgressListener method which is being
     79   *        delegated.
     80   * @param {*} args
     81   *        The arguments to pass to the delegated listener.
     82   * @private
     83   */
     84  delegate(method, ...args) {
     85    if (this.listener[method]) {
     86      this.listener[method](this.browser, ...args);
     87    }
     88  }
     89 
     90  onLocationChange(webProgress, request, locationURI, flags) {
     91    const window = this.browser.ownerGlobal;
     92    // GeckoView windows can become popups at any moment, so we need to check
     93    // here
     94    if (!windowTracker.isBrowserWindow(window)) {
     95      return;
     96    }
     97 
     98    this.delegate("onLocationChange", webProgress, request, locationURI, flags);
     99  }
    100  onStateChange(webProgress, request, stateFlags, status) {
    101    this.delegate("onStateChange", webProgress, request, stateFlags, status);
    102  }
    103 }
    104 
    105 const PROGRESS_LISTENER_FLAGS =
    106  Ci.nsIWebProgress.NOTIFY_STATE_NETWORK | Ci.nsIWebProgress.NOTIFY_LOCATION;
    107 
    108 class ProgressListenerWrapper {
    109  constructor(window, listener) {
    110    this.listener = new BrowserProgressListener(
    111      window.browser,
    112      listener,
    113      PROGRESS_LISTENER_FLAGS
    114    );
    115  }
    116 
    117  destroy() {
    118    this.listener.destroy();
    119  }
    120 }
    121 
    122 class WindowTracker extends WindowTrackerBase {
    123  constructor(...args) {
    124    super(...args);
    125 
    126    this.progressListeners = new DefaultWeakMap(() => new WeakMap());
    127  }
    128 
    129  getCurrentWindow(context) {
    130    // In GeckoView the popup is on a separate window so getCurrentWindow for
    131    // the popup should return whatever is the topWindow.
    132    if (context?.viewType === "popup") {
    133      return this.topWindow;
    134    }
    135    return super.getCurrentWindow(context);
    136  }
    137 
    138  get topWindow() {
    139    return mobileWindowTracker.topWindow;
    140  }
    141 
    142  get topNonPBWindow() {
    143    return mobileWindowTracker.topNonPBWindow;
    144  }
    145 
    146  isBrowserWindow(window) {
    147    const { documentElement } = window.document;
    148    return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
    149  }
    150 
    151  addProgressListener(window, listener) {
    152    const listeners = this.progressListeners.get(window);
    153    if (!listeners.has(listener)) {
    154      const wrapper = new ProgressListenerWrapper(window, listener);
    155      listeners.set(listener, wrapper);
    156    }
    157  }
    158 
    159  removeProgressListener(window, listener) {
    160    const listeners = this.progressListeners.get(window);
    161    const wrapper = listeners.get(listener);
    162    if (wrapper) {
    163      wrapper.destroy();
    164      listeners.delete(listener);
    165    }
    166  }
    167 }
    168 
    169 /**
    170 * Helper to create an event manager which listens for an event in the Android
    171 * global EventDispatcher, and calls the given listener function whenever the
    172 * event is received. That listener function receives a `fire` object,
    173 * which it can use to dispatch events to the extension, and an object
    174 * detailing the EventDispatcher event that was received.
    175 *
    176 * @param {BaseContext} context
    177 *        The extension context which the event manager belongs to.
    178 * @param {string} name
    179 *        The API name of the event manager, e.g.,"runtime.onMessage".
    180 * @param {string} event
    181 *        The name of the EventDispatcher event to listen for.
    182 * @param {Function} listener
    183 *        The listener function to call when an EventDispatcher event is
    184 *        recieved.
    185 *
    186 * @returns {object} An injectable api for the new event.
    187 */
    188 global.makeGlobalEvent = function makeGlobalEvent(
    189  context,
    190  name,
    191  event,
    192  listener
    193 ) {
    194  return new EventManager({
    195    context,
    196    name,
    197    register: fire => {
    198      const listener2 = {
    199        onEvent(event, data) {
    200          listener(fire, data);
    201        },
    202      };
    203 
    204      EventDispatcher.instance.registerListener(listener2, [event]);
    205      return () => {
    206        EventDispatcher.instance.unregisterListener(listener2, [event]);
    207      };
    208    },
    209  }).api();
    210 };
    211 
    212 class TabTracker extends TabTrackerBase {
    213  init() {
    214    if (this.initialized) {
    215      return;
    216    }
    217    this.initialized = true;
    218 
    219    windowTracker.addOpenListener(window => {
    220      const nativeTab = window.tab;
    221      this.emit("tab-created", { nativeTab });
    222    });
    223 
    224    windowTracker.addCloseListener(window => {
    225      const { tab: nativeTab, browser } = window;
    226      const { windowId, tabId } = this.getBrowserData(browser);
    227      this.emit("tab-removed", {
    228        nativeTab,
    229        tabId,
    230        windowId,
    231        // In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window.
    232        // Until we have a meaningful way to group tabs (and close multiple tabs at once),
    233        // let's use isWindowClosing: false
    234        isWindowClosing: false,
    235      });
    236    });
    237  }
    238 
    239  getId(nativeTab) {
    240    return nativeTab.id;
    241  }
    242 
    243  getTab(id, default_ = undefined) {
    244    const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
    245    const window = windowTracker.getWindow(windowId, null, false);
    246 
    247    if (window) {
    248      const { tab } = window;
    249      if (tab) {
    250        return tab;
    251      }
    252    }
    253 
    254    if (default_ !== undefined) {
    255      return default_;
    256    }
    257    throw new ExtensionError(`Invalid tab ID: ${id}`);
    258  }
    259 
    260  getBrowserData(browser) {
    261    const window = browser.ownerGlobal;
    262    const tab = window?.tab;
    263    if (!tab) {
    264      return {
    265        tabId: -1,
    266        windowId: -1,
    267      };
    268    }
    269 
    270    const windowId = windowTracker.getId(window);
    271 
    272    if (!windowTracker.isBrowserWindow(window)) {
    273      return {
    274        windowId,
    275        tabId: -1,
    276      };
    277    }
    278 
    279    return {
    280      windowId,
    281      tabId: this.getId(tab),
    282    };
    283  }
    284 
    285  getBrowserDataForContext(context) {
    286    if (["tab", "background"].includes(context.viewType)) {
    287      return this.getBrowserData(context.xulBrowser);
    288    } else if (context.viewType === "popup") {
    289      const chromeWindow = windowTracker.getCurrentWindow(context);
    290      const windowId = chromeWindow ? windowTracker.getId(chromeWindow) : -1;
    291      return { tabId: -1, windowId };
    292    }
    293 
    294    return { tabId: -1, windowId: -1 };
    295  }
    296 
    297  get activeTab() {
    298    const window = windowTracker.topWindow;
    299    if (window) {
    300      return window.tab;
    301    }
    302    return null;
    303  }
    304 }
    305 
    306 windowTracker = new WindowTracker();
    307 const tabTracker = new TabTracker();
    308 
    309 Object.assign(global, { tabTracker, windowTracker });
    310 
    311 class Tab extends TabBase {
    312  get _favIconUrl() {
    313    return undefined;
    314  }
    315 
    316  get attention() {
    317    return false;
    318  }
    319 
    320  get audible() {
    321    return this.nativeTab.playingAudio;
    322  }
    323 
    324  get browser() {
    325    return this.nativeTab.browser;
    326  }
    327 
    328  get discarded() {
    329    return this.browser.getAttribute("pending") === "true";
    330  }
    331 
    332  get cookieStoreId() {
    333    return getCookieStoreIdForTab(this, this.nativeTab);
    334  }
    335 
    336  get height() {
    337    return this.browser.clientHeight;
    338  }
    339 
    340  get incognito() {
    341    return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
    342  }
    343 
    344  get index() {
    345    return 0;
    346  }
    347 
    348  get mutedInfo() {
    349    return { muted: false };
    350  }
    351 
    352  get lastAccessed() {
    353    return this.nativeTab.lastTouchedAt;
    354  }
    355 
    356  get pinned() {
    357    return false;
    358  }
    359 
    360  get active() {
    361    return this.nativeTab.getActive();
    362  }
    363 
    364  get highlighted() {
    365    return this.active;
    366  }
    367 
    368  get status() {
    369    if (this.browser.webProgress.isLoadingDocument) {
    370      return "loading";
    371    }
    372    return "complete";
    373  }
    374 
    375  get successorTabId() {
    376    return -1;
    377  }
    378 
    379  get groupId() {
    380    return -1;
    381  }
    382 
    383  get width() {
    384    return this.browser.clientWidth;
    385  }
    386 
    387  get window() {
    388    return this.browser.ownerGlobal;
    389  }
    390 
    391  get windowId() {
    392    return windowTracker.getId(this.window);
    393  }
    394 
    395  // TODO: Just return false for these until properly implemented on Android.
    396  // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
    397  get isArticle() {
    398    return false;
    399  }
    400 
    401  get isInReaderMode() {
    402    return false;
    403  }
    404 
    405  get hidden() {
    406    return false;
    407  }
    408 
    409  get autoDiscardable() {
    410    // This property reflects whether the browser is allowed to auto-discard.
    411    // Since extensions cannot do so on Android, we return true here.
    412    return true;
    413  }
    414 
    415  get sharingState() {
    416    return {
    417      screen: undefined,
    418      microphone: false,
    419      camera: false,
    420    };
    421  }
    422 }
    423 
    424 // Manages tab-specific context data and dispatches tab select and close events.
    425 class TabContext extends EventEmitter {
    426  constructor(getDefaultPrototype) {
    427    super();
    428 
    429    windowTracker.addListener("progress", this);
    430 
    431    this.getDefaultPrototype = getDefaultPrototype;
    432    this.tabData = new Map();
    433  }
    434 
    435  onLocationChange(browser, webProgress, request, locationURI, flags) {
    436    if (!webProgress.isTopLevel) {
    437      // Only pageAction and browserAction are consuming the "location-change" event
    438      // to update their per-tab status, and they should only do so in response of
    439      // location changes related to the top level frame (See Bug 1493470 for a rationale).
    440      return;
    441    }
    442    const { tab } = browser.ownerGlobal;
    443    // fromBrowse will be false in case of e.g. a hash change or history.pushState
    444    const fromBrowse = !(
    445      flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
    446    );
    447    this.emit(
    448      "location-change",
    449      {
    450        id: tab.id,
    451        linkedBrowser: browser,
    452        // TODO: we don't support selected so we just alway say we are
    453        selected: true,
    454      },
    455      fromBrowse
    456    );
    457  }
    458 
    459  get(tabId) {
    460    if (!this.tabData.has(tabId)) {
    461      const data = Object.create(this.getDefaultPrototype(tabId));
    462      this.tabData.set(tabId, data);
    463    }
    464 
    465    return this.tabData.get(tabId);
    466  }
    467 
    468  clear(tabId) {
    469    this.tabData.delete(tabId);
    470  }
    471 
    472  shutdown() {
    473    windowTracker.removeListener("progress", this);
    474  }
    475 }
    476 
    477 class Window extends WindowBase {
    478  get focused() {
    479    return this.window.document.hasFocus();
    480  }
    481 
    482  isCurrentFor(context) {
    483    // In GeckoView the popup is on a separate window so the current window for
    484    // the popup is whatever is the topWindow.
    485    if (context?.viewType === "popup") {
    486      return mobileWindowTracker.topWindow == this.window;
    487    }
    488    return super.isCurrentFor(context);
    489  }
    490 
    491  get top() {
    492    return this.window.screenY;
    493  }
    494 
    495  get left() {
    496    return this.window.screenX;
    497  }
    498 
    499  get width() {
    500    return this.window.outerWidth;
    501  }
    502 
    503  get height() {
    504    return this.window.outerHeight;
    505  }
    506 
    507  get incognito() {
    508    return PrivateBrowsingUtils.isWindowPrivate(this.window);
    509  }
    510 
    511  get alwaysOnTop() {
    512    return false;
    513  }
    514 
    515  get isLastFocused() {
    516    return this.window === windowTracker.topWindow;
    517  }
    518 
    519  get state() {
    520    return "fullscreen";
    521  }
    522 
    523  *getTabs() {
    524    yield this.activeTab;
    525  }
    526 
    527  *getHighlightedTabs() {
    528    yield this.activeTab;
    529  }
    530 
    531  get activeTab() {
    532    const { tabManager } = this.extension;
    533    return tabManager.getWrapper(this.window.tab);
    534  }
    535 
    536  getTabAtIndex(index) {
    537    if (index == 0) {
    538      return this.activeTab;
    539    }
    540  }
    541 }
    542 
    543 Object.assign(global, { Tab, TabContext, Window });
    544 
    545 class TabManager extends TabManagerBase {
    546  get(tabId, default_ = undefined) {
    547    const nativeTab = tabTracker.getTab(tabId, default_);
    548 
    549    if (nativeTab) {
    550      return this.getWrapper(nativeTab);
    551    }
    552    return default_;
    553  }
    554 
    555  addActiveTabPermission(nativeTab = tabTracker.activeTab) {
    556    return super.addActiveTabPermission(nativeTab);
    557  }
    558 
    559  revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
    560    return super.revokeActiveTabPermission(nativeTab);
    561  }
    562 
    563  canAccessTab(nativeTab) {
    564    return (
    565      this.extension.privateBrowsingAllowed ||
    566      !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
    567    );
    568  }
    569 
    570  wrapTab(nativeTab) {
    571    return new Tab(this.extension, nativeTab, nativeTab.id);
    572  }
    573 }
    574 
    575 class WindowManager extends WindowManagerBase {
    576  get(windowId, context) {
    577    const window = windowTracker.getWindow(windowId, context);
    578 
    579    return this.getWrapper(window);
    580  }
    581 
    582  *getAll(context) {
    583    for (const window of windowTracker.browserWindows()) {
    584      if (!this.canAccessWindow(window, context)) {
    585        continue;
    586      }
    587      const wrapped = this.getWrapper(window);
    588      if (wrapped) {
    589        yield wrapped;
    590      }
    591    }
    592  }
    593 
    594  wrapWindow(window) {
    595    return new Window(this.extension, window, windowTracker.getId(window));
    596  }
    597 }
    598 
    599 // eslint-disable-next-line mozilla/balanced-listeners
    600 extensions.on("startup", (type, extension) => {
    601  defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
    602  defineLazyGetter(
    603    extension,
    604    "windowManager",
    605    () => new WindowManager(extension)
    606  );
    607 });
    608 
    609 /* eslint-disable mozilla/balanced-listeners */
    610 extensions.on("page-shutdown", (type, context) => {
    611  if (context.viewType == "tab") {
    612    const window = context.xulBrowser.ownerGlobal;
    613    if (!windowTracker.isBrowserWindow(window)) {
    614      // Content in non-browser window, e.g. ContentPage in xpcshell uses
    615      // chrome://extensions/content/dummy.xhtml as the window.
    616      return;
    617    }
    618    GeckoViewTabBridge.closeTab({
    619      window,
    620      extensionId: context.extension.id,
    621    });
    622  }
    623 });
    624 /* eslint-enable mozilla/balanced-listeners */
    625 
    626 global.openOptionsPage = async extension => {
    627  const { optionsPageProperties } = extension;
    628  const extensionId = extension.id;
    629 
    630  if (optionsPageProperties.open_in_tab) {
    631    // Delegate new tab creation and open the options page in the new tab.
    632    const tab = await GeckoViewTabBridge.createNewTab({
    633      extensionId,
    634      createProperties: {
    635        url: optionsPageProperties.page,
    636        active: true,
    637      },
    638    });
    639 
    640    const { browser } = tab;
    641    const loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
    642 
    643    browser.fixupAndLoadURIString(optionsPageProperties.page, {
    644      loadFlags,
    645      triggeringPrincipal: extension.principal,
    646    });
    647 
    648    const newWindow = browser.ownerGlobal;
    649    mobileWindowTracker.setTabActive(newWindow, true);
    650    return;
    651  }
    652 
    653  // Delegate option page handling to the app.
    654  return GeckoViewTabBridge.openOptionsPage(extensionId);
    655 };