tor-browser

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

TabNotesController.sys.mjs (9036B)


      1 /**
      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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 /** @import { CanonicalURLParent } from "./CanonicalURLParent.sys.mjs" */
      9 
     10 const lazy = {};
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     13  TabNotes: "moz-src:///browser/components/tabnotes/TabNotes.sys.mjs",
     14 });
     15 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
     16  return console.createInstance({
     17    prefix: "TabNotes",
     18    maxLogLevel: Services.prefs.getBoolPref("browser.tabs.notes.debug", false)
     19      ? "Debug"
     20      : "Warn",
     21  });
     22 });
     23 XPCOMUtils.defineLazyPreferenceGetter(
     24  lazy,
     25  "TAB_NOTES_ENABLED",
     26  "browser.tabs.notes.enabled",
     27  false
     28 );
     29 
     30 const EVENTS = [
     31  "CanonicalURL:Identified",
     32  "TabNote:Created",
     33  "TabNote:Edited",
     34  "TabNote:Removed",
     35 ];
     36 
     37 /**
     38 * Orchestrates the tab notes life cycle.
     39 *
     40 * Singleton class within Firefox that observes tab notes-related topics,
     41 * listens for tab notes-related events, and ensures that the state of the
     42 * tabbrowser stays in sync with the TabNotes repository.
     43 *
     44 * Registers with the category manager in order to initialize on Firefox
     45 * startup and be notified when windows are opened/closed.
     46 *
     47 * @see https://firefox-source-docs.mozilla.org/browser/CategoryManagerIndirection.html
     48 */
     49 class TabNotesControllerClass {
     50  /**
     51   * Registered with `browser-first-window-ready` to be notified of
     52   * app startup.
     53   *
     54   * @see tabnotes.manifest
     55   */
     56  init() {
     57    if (lazy.TAB_NOTES_ENABLED) {
     58      lazy.TabNotes.init();
     59    } else {
     60      lazy.logConsole.info("Tab notes disabled");
     61    }
     62  }
     63 
     64  /**
     65   * Registered with `browser-window-delayed-startup` to be notified of new
     66   * windows.
     67   *
     68   * @param {Window} win
     69   * @see tabnotes.manifest
     70   */
     71  registerWindow(win) {
     72    if (lazy.TAB_NOTES_ENABLED) {
     73      EVENTS.forEach(eventName => win.addEventListener(eventName, this));
     74      win.gBrowser.addTabsProgressListener(this);
     75      lazy.logConsole.debug("registerWindow", EVENTS, win);
     76    }
     77  }
     78 
     79  /**
     80   * Registered with `browser-window-unload` to be notified of unloaded windows.
     81   *
     82   * @param {Window} win
     83   * @see tabnotes.manifest
     84   */
     85  unregisterWindow(win) {
     86    if (lazy.TAB_NOTES_ENABLED) {
     87      EVENTS.forEach(eventName => win.removeEventListener(eventName, this));
     88      win.gBrowser.removeTabsProgressListener(this);
     89      lazy.logConsole.debug("unregisterWindow", EVENTS, win);
     90    }
     91  }
     92 
     93  /**
     94   * Registered with `browser-quit-application-granted` to be notified of
     95   * app shutdown.
     96   *
     97   * @see tabnotes.manifest
     98   */
     99  quit() {
    100    if (lazy.TAB_NOTES_ENABLED) {
    101      lazy.TabNotes.deinit();
    102    }
    103  }
    104 
    105  /**
    106   * @param {CanonicalURLIdentifiedEvent|TabNoteCreatedEvent|TabNoteEditedEvent|TabNoteRemovedEvent} event
    107   */
    108  handleEvent(event) {
    109    switch (event.type) {
    110      case "CanonicalURL:Identified":
    111        {
    112          // A browser identified its canonical URL, so we can determine whether
    113          // the tab has an associated note and should therefore display a tab
    114          // notes icon.
    115          const browser = event.target;
    116          const { canonicalUrl } = event.detail;
    117          const gBrowser = browser.getTabBrowser();
    118          const tab = gBrowser.getTabForBrowser(browser);
    119          tab.canonicalUrl = canonicalUrl;
    120          lazy.TabNotes.has(tab).then(hasTabNote => {
    121            tab.hasTabNote = hasTabNote;
    122          });
    123 
    124          lazy.logConsole.debug("CanonicalURL:Identified", tab, canonicalUrl);
    125        }
    126        break;
    127      case "TabNote:Created":
    128        {
    129          const { telemetrySource } = event.detail;
    130          if (telemetrySource) {
    131            Glean.tabNotes.added.record({
    132              source: telemetrySource,
    133            });
    134          }
    135          // A new tab note was created for a specific canonical URL. Ensure that
    136          // all tabs with the same canonical URL also indicate that there is a
    137          // tab note.
    138          const { canonicalUrl } = event.target;
    139          for (const win of lazy.BrowserWindowTracker.orderedWindows) {
    140            for (const tab of win.gBrowser.tabs) {
    141              if (tab.canonicalUrl == canonicalUrl) {
    142                tab.hasTabNote = true;
    143              }
    144            }
    145          }
    146          lazy.logConsole.debug("TabNote:Created", canonicalUrl);
    147        }
    148        break;
    149      case "TabNote:Edited":
    150        {
    151          const { canonicalUrl } = event.target;
    152          const { telemetrySource } = event.detail;
    153          if (telemetrySource) {
    154            Glean.tabNotes.edited.record({
    155              source: telemetrySource,
    156            });
    157          }
    158          lazy.logConsole.debug("TabNote:Edited", canonicalUrl);
    159        }
    160        break;
    161      case "TabNote:Removed":
    162        {
    163          const { telemetrySource, note } = event.detail;
    164          const now = Temporal.Now.instant();
    165          const noteAgeHours = Math.round(
    166            now.since(note.created).total("hours")
    167          );
    168          if (telemetrySource) {
    169            Glean.tabNotes.deleted.record({
    170              source: telemetrySource,
    171              note_age_hours: noteAgeHours,
    172            });
    173          }
    174 
    175          // A new tab note was removed from a specific canonical URL. Ensure that
    176          // all tabs with the same canonical URL also indicate that there is no
    177          // longer a tab note.
    178          const { canonicalUrl } = event.target;
    179          for (const win of lazy.BrowserWindowTracker.orderedWindows) {
    180            for (const tab of win.gBrowser.tabs) {
    181              if (tab.canonicalUrl == canonicalUrl) {
    182                tab.hasTabNote = false;
    183              }
    184            }
    185          }
    186          lazy.logConsole.debug("TabNote:Removed", canonicalUrl);
    187        }
    188        break;
    189    }
    190  }
    191 
    192  /**
    193   * Invoked by Tabbrowser after we register with `Tabbrowser.addProgressListener`.
    194   *
    195   * Clears the tab note icon and canonical URL from a tab when navigation starts
    196   * within a tab.
    197   *
    198   * The CanonicalURL actor running in this tab's browser will report the canonical
    199   * URL of the new destination in the browser, which will determine whether the
    200   * new destination has an associated tab note.
    201   *
    202   * @type {TabbrowserWebProgressListener<"onLocationChange">}
    203   */
    204  onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) {
    205    // Tab notes only apply to the top-level frames loaded in tabs.
    206    if (!aWebProgress.isTopLevel) {
    207      return;
    208    }
    209 
    210    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
    211      if (
    212        aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD ||
    213        aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY
    214      ) {
    215        // User is reloading/returning to the same document via history. We
    216        // can count on CanonicalURLChild to listen for `pageshow` and tell us
    217        // about the canonical URL at the new location.
    218        lazy.logConsole.debug(
    219          "reload/history navigation, waiting for pageshow",
    220          aLocation.spec
    221        );
    222        return;
    223      }
    224 
    225      if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_HASHCHANGE) {
    226        // The web site modified the hash/fragment identifier part of the URL
    227        // directly. TODO: determine how and whether to handle `hashchange`.
    228        lazy.logConsole.debug("fragment identifier changed", aLocation.spec);
    229        return;
    230      }
    231 
    232      if (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
    233        // Web page is using `history.pushState()` to change URLs. There isn't
    234        // a way for CanonicalURLChild to detect this in the content process,
    235        // so we need to ask it to recalculate canonical URLs to see if they
    236        // changed.
    237        /** @type {CanonicalURLParent|undefined} */
    238        let parent =
    239          aBrowser.browsingContext?.currentWindowGlobal.getExistingActor(
    240            "CanonicalURL"
    241          );
    242 
    243        if (parent) {
    244          parent.sendAsyncMessage("CanonicalURL:Detect");
    245          lazy.logConsole.debug(
    246            "requesting CanonicalURL:Detect due to history.pushState",
    247            aLocation.spec
    248          );
    249        }
    250 
    251        return;
    252      }
    253 
    254      // General same document case: we are navigating in the same document,
    255      // so the tab note indicator does not need to change.
    256      return;
    257    }
    258 
    259    // General case: we are doing normal navigation to another URL, so we
    260    // clear the canonical URL/tab note state on the tab and wait for
    261    // `CanonicalURL:Identified` to tell us whether the new location has
    262    // a tab note.
    263    const tab = aBrowser.ownerGlobal.gBrowser.getTabForBrowser(aBrowser);
    264    tab.canonicalUrl = undefined;
    265    tab.hasTabNote = false;
    266    lazy.logConsole.debug("clear tab note due to location change", tab);
    267  }
    268 }
    269 
    270 export const TabNotesController = new TabNotesControllerClass();