tor-browser

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

ContentAnalysis.sys.mjs (39677B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set ts=2 et sw=2 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
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 // @ts-check
      7 
      8 /**
      9 * Contains elements of the Content Analysis UI, which are integrated into
     10 * various browser behaviors (uploading, downloading, printing, etc) that
     11 * require content analysis to be done.
     12 * The content analysis itself is done by the clients of this script, who
     13 * use nsIContentAnalysis to talk to the external CA system.
     14 */
     15 
     16 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     17 
     18 const lazy = {};
     19 let internalContentAnalysisService = undefined;
     20 
     21 ChromeUtils.defineESModuleGetters(lazy, {
     22  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
     23  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
     24  PanelMultiView:
     25    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     26  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     27 });
     28 
     29 XPCOMUtils.defineLazyPreferenceGetter(
     30  lazy,
     31  "silentNotifications",
     32  "browser.contentanalysis.silent_notifications",
     33  false
     34 );
     35 
     36 XPCOMUtils.defineLazyPreferenceGetter(
     37  lazy,
     38  "agentName",
     39  "browser.contentanalysis.agent_name",
     40  "A DLP agent"
     41 );
     42 
     43 XPCOMUtils.defineLazyPreferenceGetter(
     44  lazy,
     45  "showBlockedResult",
     46  "browser.contentanalysis.show_blocked_result",
     47  true
     48 );
     49 
     50 export const ContentAnalysis = {
     51  _SHOW_NOTIFICATIONS: true,
     52 
     53  _SHOW_DIALOGS: false,
     54 
     55  _SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS: 250,
     56 
     57  _SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS: 3 * 1000,
     58 
     59  _RESULT_NOTIFICATION_TIMEOUT_MS: 5 * 60 * 1000, // 5 min
     60 
     61  _RESULT_NOTIFICATION_FAST_TIMEOUT_MS: 60 * 1000, // 1 min
     62 
     63  PROMPTID_PREFIX: "ContentAnalysisSlowDialog-",
     64 
     65  isInitialized: false,
     66 
     67  /**
     68   * @typedef {object} NotificationInfo - information about the busy dialog itself that is showing
     69   * @property {*} [close] - Method to close the native notification
     70   * @property {BrowsingContext} [dialogBrowsingContext] - browsing context where the
     71   *                                                       confirm() dialog is shown
     72   */
     73 
     74  /**
     75   * @typedef {object} BusyDialogInfo - information about a busy dialog that is either showing or will
     76   *                                    will be shown after a delay.
     77   * @property {string} userActionId - The userActionId of the request
     78   * @property {Set<string>} requestTokenSet - The set of requestTokens associated with the userActionId
     79   * @property {*} [timer] - Result of a setTimeout() call that can be used to cancel the showing of the busy
     80   *                         dialog if it has not been displayed yet.
     81   * @property {NotificationInfo} [notification] - Information about the busy dialog that is being shown.
     82   */
     83 
     84  /**
     85   * @type {Map<string, BusyDialogInfo>}
     86   *
     87   * Maps string UserActionId to info about the busy dialog.
     88   */
     89  userActionToBusyDialogMap: new Map(),
     90 
     91  /**
     92   * @typedef {object} ResourceNameOrOperationType
     93   * @property {string} [name] - the name of the resource
     94   * @property {number} [operationType] - the type of operation
     95   */
     96 
     97  /**
     98   * @typedef {object} RequestInfo
     99   * @property {CanonicalBrowsingContext?} browsingContext - browsing context where the request was sent from
    100   * @property {ResourceNameOrOperationType} resourceNameOrOperationType - name of the operation
    101   */
    102 
    103  /**
    104   * @type {Map<string, RequestInfo>}
    105   */
    106  requestTokenToRequestInfo: new Map(),
    107 
    108  /**
    109   * @type {Set<string>}
    110   */
    111  warnDialogRequestTokens: new Set(),
    112 
    113  /**
    114   * The nsIContentAnalysis to use instead of lazy.gContentAnalysis. Should
    115   * only be used for tests.
    116   *
    117   * @type {nsIContentAnalysis?}
    118   */
    119  mockContentAnalysisForTest: undefined,
    120 
    121  /**
    122   * The nsIContentAnalysis to use. Nothing else in this file should
    123   * use lazy.gContentAnalysis.
    124   *
    125   * @returns {nsIContentAnalysis}
    126   */
    127  get contentAnalysis() {
    128    if (this.mockContentAnalysisForTest) {
    129      return this.mockContentAnalysisForTest;
    130    }
    131    if (!internalContentAnalysisService) {
    132      internalContentAnalysisService = Cc[
    133        "@mozilla.org/contentanalysis;1"
    134      ].getService(Ci.nsIContentAnalysis);
    135    }
    136    return internalContentAnalysisService;
    137  },
    138 
    139  /**
    140   * Sets the nsIContentAnalysis to use. Should only be used for tests.
    141   *
    142   * @param {nsIContentAnalysis?} contentAnalysis
    143   */
    144  setMockContentAnalysisForTest(contentAnalysis) {
    145    this.mockContentAnalysisForTest = contentAnalysis;
    146  },
    147 
    148  /**
    149   * Registers for various messages/events that will indicate the
    150   * need for communicating something to the user.
    151   *
    152   * @param {Window} window - The window to monitor
    153   */
    154  initialize(window) {
    155    if (!this.contentAnalysis.isActive) {
    156      this.uninitialize();
    157      return;
    158    }
    159    let doc = window.document;
    160    if (!this.isInitialized) {
    161      this.isInitialized = true;
    162      this.initializeObservers();
    163 
    164      ChromeUtils.defineLazyGetter(this, "l10n", function () {
    165        return new Localization(
    166          ["branding/brand.ftl", "toolkit/contentanalysis/contentanalysis.ftl"],
    167          true
    168        );
    169      });
    170    }
    171 
    172    // Do this even if initialized so the icon shows up on new windows, not just the
    173    // first one.
    174    for (let indicator of doc.getElementsByClassName(
    175      "content-analysis-indicator"
    176    )) {
    177      doc.l10n.setAttributes(indicator, "content-analysis-indicator-tooltip", {
    178        agentName: lazy.agentName,
    179      });
    180    }
    181    doc.documentElement.setAttribute("contentanalysisactive", "true");
    182  },
    183 
    184  async uninitialize() {
    185    if (this.isInitialized) {
    186      this.isInitialized = false;
    187      this.requestTokenToRequestInfo.clear();
    188      this.userActionToBusyDialogMap.clear();
    189      this.uninitializeObservers();
    190    }
    191  },
    192 
    193  /**
    194   * Register UI for CA events.
    195   */
    196  initializeObservers() {
    197    Services.obs.addObserver(this, "dlp-request-made");
    198    Services.obs.addObserver(this, "dlp-response");
    199    Services.obs.addObserver(this, "quit-application");
    200    Services.obs.addObserver(this, "quit-application-granted");
    201    Services.obs.addObserver(this, "quit-application-requested");
    202  },
    203 
    204  /**
    205   * Unregister UI for CA events.
    206   */
    207  uninitializeObservers() {
    208    Services.obs.removeObserver(this, "dlp-request-made");
    209    Services.obs.removeObserver(this, "dlp-response");
    210    Services.obs.removeObserver(this, "quit-application");
    211    Services.obs.removeObserver(this, "quit-application-granted");
    212    Services.obs.removeObserver(this, "quit-application-requested");
    213  },
    214 
    215  // nsIObserver
    216  async observe(aSubj, aTopic, _aData) {
    217    switch (aTopic) {
    218      case "quit-application-requested": {
    219        if (aSubj.data) {
    220          // something has already cancelled the quit operation,
    221          // so we don't need to do anything.
    222          return;
    223        }
    224        let pendingRequestInfos = this._getAllSlowCARequestInfos();
    225        let requestDescriptions = Array.from(
    226          pendingRequestInfos.flatMap(info =>
    227            info
    228              ? [
    229                  this._getResourceNameFromNameOrOperationType(
    230                    info.resourceNameOrOperationType
    231                  ),
    232                ]
    233              : []
    234          )
    235        );
    236        if (!requestDescriptions.length) {
    237          return;
    238        }
    239        let messageBody = this.l10n.formatValueSync(
    240          "contentanalysis-inprogress-quit-message"
    241        );
    242        messageBody = messageBody + "\n\n";
    243        messageBody += requestDescriptions.join("\n");
    244        let buttonSelected = Services.prompt.confirmEx(
    245          null,
    246          this.l10n.formatValueSync("contentanalysis-inprogress-quit-title"),
    247          messageBody,
    248          Ci.nsIPromptService.BUTTON_POS_0 *
    249            Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
    250            Ci.nsIPromptService.BUTTON_POS_1 *
    251              Ci.nsIPromptService.BUTTON_TITLE_CANCEL +
    252            Ci.nsIPromptService.BUTTON_POS_0_DEFAULT,
    253          this.l10n.formatValueSync(
    254            "contentanalysis-inprogress-quit-yesbutton"
    255          ),
    256          null,
    257          null,
    258          null,
    259          { value: false }
    260        );
    261        if (buttonSelected === 1) {
    262          // Cancel the quit operation
    263          aSubj.data = true;
    264        } else {
    265          // Ideally we would wait until "quit-application" to cancel outstanding
    266          // DLP requests, but the "DLP busy" or "DLP blocked" dialog can block the
    267          // main thread, thus preventing the "quit-application" from being sent,
    268          // which causes a shutdownhang. (bug 1899703)
    269          this.contentAnalysis.cancelAllRequests(true);
    270        }
    271        break;
    272      }
    273      // Note that we do this in quit-application-granted instead of quit-application
    274      // because otherwise we can get a shutdownhang if WARN dialogs are showing and
    275      // the user quits via keyboard or the hamburger menu (bug 1959966)
    276      case "quit-application-granted": {
    277        // We're quitting, so respond false to all WARN dialogs.
    278        let requestTokensToCancel = this.warnDialogRequestTokens;
    279        // Clear this first so the handler showing the dialog will know not
    280        // to call respondToWarnDialog() again.
    281        this.warnDialogRequestTokens = new Set();
    282        for (let warnDialogRequestToken of requestTokensToCancel) {
    283          this.contentAnalysis.respondToWarnDialog(
    284            warnDialogRequestToken,
    285            false
    286          );
    287        }
    288        break;
    289      }
    290      case "quit-application": {
    291        this.uninitialize();
    292        break;
    293      }
    294      case "dlp-request-made":
    295        {
    296          const request = aSubj.QueryInterface(Ci.nsIContentAnalysisRequest);
    297          if (!request) {
    298            console.error(
    299              "Showing in-browser Content Analysis notification but no request was passed"
    300            );
    301            return;
    302          }
    303          let browsingContext = request.windowGlobalParent?.browsingContext;
    304          if (
    305            !browsingContext &&
    306            request.operationTypeForDisplay !==
    307              Ci.nsIContentAnalysisRequest.eDownload
    308          ) {
    309            throw new Error(
    310              "Got dlp-request-made message but couldn't find a browsingContext!"
    311            );
    312          }
    313 
    314          // Start timer that, when it expires,
    315          // presents a "slow CA check" message.
    316          let resourceNameOrOperationType =
    317            this._getResourceNameOrOperationTypeFromRequest(request, false);
    318          this.requestTokenToRequestInfo.set(request.requestToken, {
    319            browsingContext,
    320            resourceNameOrOperationType,
    321          });
    322          this._queueSlowCAMessage(
    323            request,
    324            resourceNameOrOperationType,
    325            browsingContext
    326          );
    327        }
    328        break;
    329      case "dlp-response": {
    330        const response = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse);
    331        // Cancels timer or slow message UI,
    332        // if present, and possibly presents the CA verdict.
    333        if (!response) {
    334          throw new Error(
    335            "Got dlp-response message but no response object was passed"
    336          );
    337        }
    338 
    339        let windowAndResourceNameOrOperationType =
    340          this.requestTokenToRequestInfo.get(response.requestToken);
    341        if (!windowAndResourceNameOrOperationType) {
    342          // We may get multiple responses, for example, if we are blocked or
    343          // canceled after receiving our verdict because we were part of a
    344          // multipart transaction.  Just ignore that.
    345          console.warn(
    346            `Got dlp-response message with unknown token ${response.requestToken} | action: ${response.action}`
    347          );
    348          return;
    349        }
    350        this.requestTokenToRequestInfo.delete(response.requestToken);
    351        this._removeSlowCAMessage(response.userActionId, response.requestToken);
    352        if (
    353          windowAndResourceNameOrOperationType.resourceNameOrOperationType
    354            ?.operationType === Ci.nsIContentAnalysisRequest.eDownload
    355        ) {
    356          // Don't show warn/block/error dialogs for downloads; they're shown
    357          // inside the downloads panel.
    358          return;
    359        }
    360        const responseResult =
    361          response?.action ?? Ci.nsIContentAnalysisResponse.eUnspecified;
    362        // Don't show dialog if this is a cached response
    363        if (!response?.isCachedResponse) {
    364          await this._showCAResult(
    365            windowAndResourceNameOrOperationType.resourceNameOrOperationType,
    366            windowAndResourceNameOrOperationType.browsingContext,
    367            response.requestToken,
    368            response.userActionId,
    369            responseResult,
    370            response.isSyntheticResponse,
    371            response.cancelError
    372          );
    373        }
    374        break;
    375      }
    376    }
    377  },
    378 
    379  /**
    380   * Shows the panel that indicates that DLP is active.
    381   *
    382   * @param {Element} element The toolbarbutton the user has clicked on
    383   * @param {*} panelUI Maintains state for the main menu panel
    384   */
    385  async showPanel(element, panelUI) {
    386    element.ownerDocument.l10n.setAttributes(
    387      lazy.PanelMultiView.getViewNode(
    388        element.ownerDocument,
    389        "content-analysis-panel-description"
    390      ),
    391      "content-analysis-panel-text-styled",
    392      { agentName: lazy.agentName }
    393    );
    394    panelUI.showSubView("content-analysis-panel", element);
    395  },
    396 
    397  /**
    398   * Closes a busy dialog
    399   *
    400   * @param {BusyDialogInfo?} caView - the busy dialog to close
    401   */
    402  _disconnectFromView(caView) {
    403    if (!caView) {
    404      return;
    405    }
    406    if (caView.timer) {
    407      lazy.clearTimeout(caView.timer);
    408    } else if (caView.notification) {
    409      if (caView.notification.close) {
    410        // native notification
    411        caView.notification.close();
    412      } else if (caView.notification.dialogBrowsingContext) {
    413        // in-browser notification
    414        let browser =
    415          caView.notification.dialogBrowsingContext.top.embedderElement;
    416        // If we're showing a dialog in the sidebar, the dialog is managed
    417        // by the embedderElement.
    418        let isSidebar =
    419          browser?.ownerGlobal?.browsingContext?.embedderElement?.id ==
    420          "sidebar";
    421        if (isSidebar) {
    422          browser = browser.ownerGlobal.browsingContext.embedderElement;
    423        }
    424        // browser will be null if the tab was closed
    425        let win = browser?.ownerGlobal;
    426        if (win) {
    427          let dialogBox = win.gBrowser.getTabDialogBox(browser);
    428          // Just close the dialog associated with this CA request.
    429          dialogBox.getTabDialogManager().abortDialogs(dialog => {
    430            return (
    431              dialog.promptID == this.PROMPTID_PREFIX + caView.userActionId
    432            );
    433          });
    434        }
    435      } else {
    436        console.error(
    437          "Unexpected content analysis notification - can't close it!"
    438        );
    439      }
    440    }
    441  },
    442 
    443  /**
    444   * Shows either a dialog or native notification or both, depending on the values of
    445   * _SHOW_DIALOGS and _SHOW_NOTIFICATIONS.
    446   *
    447   * @param {string} aMessage - Message to show
    448   * @param {CanonicalBrowsingContext?} aBrowsingContext - BrowsingContext to show the dialog in. If
    449   *                            null, the top browsing context will be used for native notifications.
    450   * @param {number} aTimeout - timeout for closing the native notification. 0 indicates it is
    451   *                            not automatically closed.
    452   * @returns {NotificationInfo?} - information about the native notification, if it has been shown.
    453   */
    454  _showMessage(aMessage, aBrowsingContext, aTimeout = 0) {
    455    if (this._SHOW_DIALOGS) {
    456      Services.prompt.asyncAlert(
    457        aBrowsingContext,
    458        Ci.nsIPrompt.MODAL_TYPE_WINDOW,
    459        this.l10n.formatValueSync("contentanalysis-alert-title"),
    460        aMessage
    461      );
    462    }
    463 
    464    if (this._SHOW_NOTIFICATIONS) {
    465      // Downloading as a "save as" operation does not provide a browsing context,
    466      // so use the the top window in that case.
    467      let topWindow =
    468        aBrowsingContext?.topChromeWindow ??
    469        aBrowsingContext?.embedderWindowGlobal.browsingContext
    470          .topChromeWindow ??
    471        lazy.BrowserWindowTracker.getTopWindow({
    472          allowFromInactiveWorkspace: true,
    473        });
    474      if (!topWindow) {
    475        console.error(
    476          "Unable to get window to show Content Analysis notification for."
    477        );
    478        return null;
    479      }
    480      const notification = new topWindow.Notification(
    481        this.l10n.formatValueSync("contentanalysis-notification-title"),
    482        { body: aMessage, silent: lazy.silentNotifications }
    483      );
    484 
    485      if (aTimeout != 0) {
    486        lazy.setTimeout(() => {
    487          notification.close();
    488        }, aTimeout);
    489      }
    490      return notification;
    491    }
    492 
    493    return null;
    494  },
    495 
    496  /**
    497   * Whether the notification should block browser interaction.
    498   *
    499   * @param {nsIContentAnalysisRequest.AnalysisType} aAnalysisType The type of DLP analysis being done.
    500   * @returns {boolean}
    501   */
    502  _shouldShowBlockingNotification(aAnalysisType) {
    503    return !(
    504      aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded ||
    505      aAnalysisType == Ci.nsIContentAnalysisRequest.ePrint
    506    );
    507  },
    508 
    509  /**
    510   * This function also transforms the nameOrOperationType so we won't have to
    511   * look it up again.
    512   *
    513   * @param {ResourceNameOrOperationType} nameOrOperationType
    514   * @returns {string}
    515   */
    516  _getResourceNameFromNameOrOperationType(nameOrOperationType) {
    517    if (!nameOrOperationType.name) {
    518      let l10nId = undefined;
    519      switch (nameOrOperationType.operationType) {
    520        case Ci.nsIContentAnalysisRequest.eClipboard:
    521          l10nId = "contentanalysis-operationtype-clipboard";
    522          break;
    523        case Ci.nsIContentAnalysisRequest.eDroppedText:
    524          l10nId = "contentanalysis-operationtype-dropped-text";
    525          break;
    526        case Ci.nsIContentAnalysisRequest.eOperationPrint:
    527          l10nId = "contentanalysis-operationtype-print";
    528          break;
    529      }
    530      if (!l10nId) {
    531        console.error(
    532          "Unknown operationTypeForDisplay: " +
    533            nameOrOperationType.operationType
    534        );
    535        return "";
    536      }
    537      nameOrOperationType.name = this.l10n.formatValueSync(l10nId);
    538    }
    539    return nameOrOperationType.name;
    540  },
    541 
    542  /**
    543   * Gets a name or operation type from a request
    544   *
    545   * @param {nsIContentAnalysisRequest} aRequest The nsIContentAnalysisRequest
    546   * @param {boolean} aStandalone Whether the message is going to be used on its own
    547   *                              line. This is used to add more context to the message
    548   *                              if a file is being uploaded or downloaded rather than
    549   *                              just the name of the file.
    550   * @returns {ResourceNameOrOperationType}
    551   */
    552  _getResourceNameOrOperationTypeFromRequest(aRequest, aStandalone) {
    553    /**
    554     * @type {ResourceNameOrOperationType}
    555     */
    556    let nameOrOperationType = {
    557      operationType: aRequest.operationTypeForDisplay,
    558    };
    559    if (
    560      aRequest.operationTypeForDisplay == Ci.nsIContentAnalysisRequest.eUpload
    561    ) {
    562      if (aStandalone) {
    563        nameOrOperationType.name = this.l10n.formatValueSync(
    564          "contentanalysis-upload-description",
    565          { filename: aRequest.fileNameForDisplay }
    566        );
    567      } else {
    568        nameOrOperationType.name = aRequest.fileNameForDisplay;
    569      }
    570    } else if (
    571      aRequest.operationTypeForDisplay == Ci.nsIContentAnalysisRequest.eDownload
    572    ) {
    573      if (aStandalone) {
    574        nameOrOperationType.name = this.l10n.formatValueSync(
    575          "contentanalysis-download-description",
    576          { filename: aRequest.fileNameForDisplay }
    577        );
    578      } else {
    579        nameOrOperationType.name = aRequest.fileNameForDisplay;
    580      }
    581    }
    582    return nameOrOperationType;
    583  },
    584 
    585  /**
    586   * Sets up an "operation is in progress" dialog to be shown after a delay,
    587   * unless one is already showing for this userActionId.
    588   *
    589   * @param {nsIContentAnalysisRequest} aRequest
    590   * @param {ResourceNameOrOperationType} aResourceNameOrOperationType
    591   * @param {CanonicalBrowsingContext?} aBrowsingContext
    592   */
    593  _queueSlowCAMessage(
    594    aRequest,
    595    aResourceNameOrOperationType,
    596    aBrowsingContext
    597  ) {
    598    let entry = this.userActionToBusyDialogMap.get(aRequest.userActionId);
    599    if (entry) {
    600      // Don't show busy dialog if another request is already doing so.
    601      entry.requestTokenSet.add(aRequest.requestToken);
    602      return;
    603    }
    604 
    605    const analysisType = aRequest.analysisType;
    606    // For operations that block browser interaction, show the "slow content analysis"
    607    // dialog faster
    608    let slowTimeoutMs = this._shouldShowBlockingNotification(analysisType)
    609      ? this._SLOW_DLP_NOTIFICATION_BLOCKING_TIMEOUT_MS
    610      : this._SLOW_DLP_NOTIFICATION_NONBLOCKING_TIMEOUT_MS;
    611 
    612    entry = {
    613      requestTokenSet: new Set([aRequest.requestToken]),
    614      userActionId: aRequest.userActionId,
    615    };
    616    this.userActionToBusyDialogMap.set(aRequest.userActionId, entry);
    617    entry.timer = lazy.setTimeout(() => {
    618      entry.timer = null;
    619      entry.notification = this._showSlowCAMessage(
    620        analysisType,
    621        aRequest,
    622        this._getSlowDialogMessage(
    623          aResourceNameOrOperationType,
    624          aRequest.userActionRequestsCount
    625        ),
    626        aBrowsingContext
    627      );
    628    }, slowTimeoutMs);
    629  },
    630 
    631  /**
    632   * Removes the Slow CA message, if it is showing
    633   *
    634   * @param {string} aUserActionId The user action ID to remove
    635   * @param {string} aRequestToken The request token to remove
    636   */
    637  _removeSlowCAMessage(aUserActionId, aRequestToken) {
    638    let entry = this.userActionToBusyDialogMap.get(aUserActionId);
    639    if (!entry) {
    640      console.error(
    641        `Couldn't find slow dialog for user action ${aUserActionId}`
    642      );
    643      return;
    644    }
    645    if (!entry.requestTokenSet.delete(aRequestToken)) {
    646      console.warn(
    647        `Couldn't find request ${aRequestToken} in slow dialog object for user action ${aUserActionId}.  Shutting down?`
    648      );
    649      return;
    650    }
    651    if (entry.requestTokenSet.size) {
    652      // Continue showing the busy dialog since other requests are still pending.
    653      return;
    654    }
    655    this.userActionToBusyDialogMap.delete(aUserActionId);
    656    this._disconnectFromView(entry);
    657  },
    658 
    659  /**
    660   * Gets all the requests that are still in progress.
    661   *
    662   * @returns {IteratorObject<RequestInfo>} Information about the requests that are still in progress
    663   */
    664  _getAllSlowCARequestInfos() {
    665    return this.userActionToBusyDialogMap
    666      .values()
    667      .flatMap(val => val.requestTokenSet)
    668      .map(requestToken => this.requestTokenToRequestInfo.get(requestToken));
    669  },
    670 
    671  /**
    672   * Show a message to the user to indicate that a CA request is taking
    673   * a long time.
    674   *
    675   * @param {nsIContentAnalysisRequest.AnalysisType} aOperation The operation
    676   * @param {nsIContentAnalysisRequest} aRequest The request that is taking a long time
    677   * @param {string} aBodyMessage Message to show in the body of the alert
    678   * @param {CanonicalBrowsingContext?} aBrowsingContext BrowsingContext to show the alert in
    679   */
    680  _showSlowCAMessage(aOperation, aRequest, aBodyMessage, aBrowsingContext) {
    681    if (!this._shouldShowBlockingNotification(aOperation)) {
    682      return this._showMessage(aBodyMessage, aBrowsingContext);
    683    }
    684 
    685    if (!aRequest) {
    686      throw new Error(
    687        "Showing in-browser Content Analysis notification but no request was passed"
    688      );
    689    }
    690 
    691    return this._showSlowCABlockingMessage(
    692      aBrowsingContext,
    693      aRequest.userActionId,
    694      aRequest.requestToken,
    695      aBodyMessage
    696    );
    697  },
    698 
    699  /**
    700   * Gets the dialog message to show for the Slow CA dialog.
    701   *
    702   * @param {ResourceNameOrOperationType} aResourceNameOrOperationType
    703   * @param {number} aNumRequests
    704   * @returns {string}
    705   */
    706  _getSlowDialogMessage(aResourceNameOrOperationType, aNumRequests) {
    707    if (aResourceNameOrOperationType.name) {
    708      let label =
    709        aNumRequests > 1
    710          ? "contentanalysis-slow-agent-dialog-body-file-and-more"
    711          : "contentanalysis-slow-agent-dialog-body-file";
    712 
    713      return this.l10n.formatValueSync(label, {
    714        agent: lazy.agentName,
    715        filename: aResourceNameOrOperationType.name,
    716        count: aNumRequests - 1,
    717      });
    718    }
    719    let l10nId = undefined;
    720    switch (aResourceNameOrOperationType.operationType) {
    721      case Ci.nsIContentAnalysisRequest.eClipboard:
    722        l10nId = "contentanalysis-slow-agent-dialog-body-clipboard";
    723        break;
    724      case Ci.nsIContentAnalysisRequest.eDroppedText:
    725        l10nId = "contentanalysis-slow-agent-dialog-body-dropped-text";
    726        break;
    727      case Ci.nsIContentAnalysisRequest.eOperationPrint:
    728        l10nId = "contentanalysis-slow-agent-dialog-body-print";
    729        break;
    730    }
    731    if (!l10nId) {
    732      console.error(
    733        "Unknown operationTypeForDisplay: ",
    734        aResourceNameOrOperationType
    735      );
    736      return "";
    737    }
    738    return this.l10n.formatValueSync(l10nId, { agent: lazy.agentName });
    739  },
    740 
    741  /**
    742   * Gets the dialog message to show when the request has an error.
    743   *
    744   * @param {ResourceNameOrOperationType} aResourceNameOrOperationType
    745   * @returns {string}
    746   */
    747  _getErrorDialogMessage(aResourceNameOrOperationType) {
    748    if (aResourceNameOrOperationType.name) {
    749      return this.l10n.formatValueSync(
    750        "contentanalysis-error-message-upload-file",
    751        { filename: aResourceNameOrOperationType.name }
    752      );
    753    }
    754    let l10nId = undefined;
    755    switch (aResourceNameOrOperationType.operationType) {
    756      case Ci.nsIContentAnalysisRequest.eClipboard:
    757        l10nId = "contentanalysis-error-message-clipboard";
    758        break;
    759      case Ci.nsIContentAnalysisRequest.eDroppedText:
    760        l10nId = "contentanalysis-error-message-dropped-text";
    761        break;
    762      case Ci.nsIContentAnalysisRequest.eOperationPrint:
    763        l10nId = "contentanalysis-error-message-print";
    764        break;
    765    }
    766    if (!l10nId) {
    767      console.error(
    768        "Unknown operationTypeForDisplay: ",
    769        aResourceNameOrOperationType
    770      );
    771      return "";
    772    }
    773    return this.l10n.formatValueSync(l10nId);
    774  },
    775 
    776  /**
    777   * Show the Slow CA blocking dialog.
    778   *
    779   * @param {BrowsingContext} aBrowsingContext
    780   * @param {string} aUserActionId
    781   * @param {string} aRequestToken
    782   * @param {string} aBodyMessage
    783   * @returns {NotificationInfo}
    784   */
    785  _showSlowCABlockingMessage(
    786    aBrowsingContext,
    787    aUserActionId,
    788    aRequestToken,
    789    aBodyMessage
    790  ) {
    791    // Note that TabDialogManager maintains a list of displaying dialogs, and so
    792    // we can pop up multiple of these and the first one will keep displaying until
    793    // it is closed, at which point the next one will display, etc.
    794    let promise = Services.prompt.asyncConfirmEx(
    795      aBrowsingContext,
    796      Ci.nsIPromptService.MODAL_TYPE_TAB,
    797      this.l10n.formatValueSync("contentanalysis-slow-agent-dialog-header"),
    798      aBodyMessage,
    799      Ci.nsIPromptService.BUTTON_POS_0 *
    800        Ci.nsIPromptService.BUTTON_TITLE_CANCEL +
    801        Ci.nsIPromptService.BUTTON_POS_1_DEFAULT +
    802        Ci.nsIPromptService.SHOW_SPINNER,
    803      null,
    804      null,
    805      null,
    806      null,
    807      false,
    808      { promptID: this.PROMPTID_PREFIX + aUserActionId }
    809    );
    810    promise
    811      .catch(() => {
    812        // need a catch clause to avoid an unhandled JS exception
    813        // when we programmatically close the dialog or close the tab.
    814      })
    815      .finally(() => {
    816        // This is also called if the tab/window is closed while a request is
    817        // in progress, in which case we need to cancel all related requests.
    818        //
    819        // If aUserActionId is still in userActionToBusyDialogMap,
    820        // this means the dialog wasn't closed by _disconnectFromView(),
    821        // so cancel the operation.
    822        if (this.userActionToBusyDialogMap.has(aUserActionId)) {
    823          this.contentAnalysis.cancelAllRequestsAssociatedWithUserAction(
    824            aUserActionId
    825          );
    826        }
    827        // Do this after checking userActionToBusyDialogMap, since
    828        // _removeSlowCAMessage() will remove the entry from
    829        // userActionToBusyDialogMap.
    830        if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
    831          // I think this is needed to clean up when the tab/window
    832          // is closed.
    833          this._removeSlowCAMessage(aUserActionId, aRequestToken);
    834        }
    835      });
    836    return {
    837      dialogBrowsingContext: aBrowsingContext,
    838    };
    839  },
    840 
    841  /**
    842   * Show a message to the user to indicate the result of a CA request.
    843   *
    844   * @param {ResourceNameOrOperationType} aResourceNameOrOperationType
    845   * @param {CanonicalBrowsingContext} aBrowsingContext
    846   * @param {string} aRequestToken
    847   * @param {string} aUserActionId
    848   * @param {number} aCAResult
    849   * @param {boolean} aIsSyntheticResponse
    850   * @param {number} aRequestCancelError
    851   * @returns {Promise<NotificationInfo?>} a notification object (if shown)
    852   */
    853  async _showCAResult(
    854    aResourceNameOrOperationType,
    855    aBrowsingContext,
    856    aRequestToken,
    857    aUserActionId,
    858    aCAResult,
    859    aIsSyntheticResponse,
    860    aRequestCancelError
    861  ) {
    862    let message = null;
    863    let timeoutMs = 0;
    864 
    865    switch (aCAResult) {
    866      case Ci.nsIContentAnalysisResponse.eAllow:
    867        // We don't need to show anything
    868        return null;
    869      case Ci.nsIContentAnalysisResponse.eReportOnly:
    870        message = await this.l10n.formatValue(
    871          "contentanalysis-genericresponse-message",
    872          {
    873            content: this._getResourceNameFromNameOrOperationType(
    874              aResourceNameOrOperationType
    875            ),
    876            response: "REPORT_ONLY",
    877          }
    878        );
    879        timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS;
    880        break;
    881      case Ci.nsIContentAnalysisResponse.eWarn: {
    882        let allow = false;
    883        try {
    884          this.warnDialogRequestTokens.add(aRequestToken);
    885          const result = await Services.prompt.asyncConfirmEx(
    886            aBrowsingContext,
    887            Ci.nsIPromptService.MODAL_TYPE_TAB,
    888            await this.l10n.formatValue("contentanalysis-warndialogtitle"),
    889            await this._warnDialogText(aResourceNameOrOperationType),
    890            Ci.nsIPromptService.BUTTON_POS_0 *
    891              Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
    892              Ci.nsIPromptService.BUTTON_POS_1 *
    893                Ci.nsIPromptService.BUTTON_TITLE_IS_STRING +
    894              Ci.nsIPromptService.BUTTON_POS_2_DEFAULT,
    895            await this.l10n.formatValue(
    896              "contentanalysis-warndialog-response-allow"
    897            ),
    898            await this.l10n.formatValue(
    899              "contentanalysis-warndialog-response-deny"
    900            ),
    901            null,
    902            null,
    903            false
    904          );
    905          allow = result.get("buttonNumClicked") === 0;
    906        } catch {
    907          // This can happen if the dialog is closed programmatically, for
    908          // example if the tab is moved to a new window.
    909          // In this case just pretend the user clicked deny, as this
    910          // emulates the behavior of cancelling when
    911          // the request is still active.
    912          allow = false;
    913        }
    914        // Note that the shutdown code in the "quit-application" handler
    915        // may have cleared out warnDialogRequestTokens and responded
    916        // to the request already, so don't call respondToWarnDialog()
    917        // if aRequestToken is not in warnDialogRequestTokens.
    918        if (this.warnDialogRequestTokens.delete(aRequestToken)) {
    919          this.contentAnalysis.respondToWarnDialog(aRequestToken, allow);
    920        }
    921        return null;
    922      }
    923      case Ci.nsIContentAnalysisResponse.eBlock: {
    924        if (!aIsSyntheticResponse && !lazy.showBlockedResult) {
    925          // Don't show anything
    926          return null;
    927        }
    928        let titleId = undefined;
    929        let body = undefined;
    930        if (aResourceNameOrOperationType.name) {
    931          titleId =
    932            aResourceNameOrOperationType.operationType ==
    933            Ci.nsIContentAnalysisRequest.eUpload
    934              ? "contentanalysis-block-dialog-title-upload-file"
    935              : "contentanalysis-block-dialog-title-download-file";
    936          body = this.l10n.formatValueSync(
    937            aResourceNameOrOperationType.operationType ==
    938              Ci.nsIContentAnalysisRequest.eUpload
    939              ? "contentanalysis-block-dialog-body-upload-file"
    940              : "contentanalysis-block-dialog-body-download-file",
    941            { filename: aResourceNameOrOperationType.name }
    942          );
    943        } else {
    944          let bodyId = undefined;
    945          let bodyHasContent = false;
    946          switch (aResourceNameOrOperationType.operationType) {
    947            case Ci.nsIContentAnalysisRequest.eClipboard: {
    948              // Unlike the cases below, this can be shown when the DLP
    949              // agent is not available.  We use a different message for that.
    950              const caInfo = await this.contentAnalysis.getDiagnosticInfo();
    951              titleId = "contentanalysis-block-dialog-title-clipboard";
    952              bodyId = caInfo.connectedToAgent
    953                ? "contentanalysis-block-dialog-body-clipboard"
    954                : "contentanalysis-no-agent-connected-message-content";
    955              bodyHasContent = true;
    956              break;
    957            }
    958            case Ci.nsIContentAnalysisRequest.eDroppedText:
    959              titleId = "contentanalysis-block-dialog-title-dropped-text";
    960              bodyId = "contentanalysis-block-dialog-body-dropped-text";
    961              break;
    962            case Ci.nsIContentAnalysisRequest.eOperationPrint:
    963              titleId = "contentanalysis-block-dialog-title-print";
    964              bodyId = "contentanalysis-block-dialog-body-print";
    965              break;
    966          }
    967          if (!titleId || !bodyId) {
    968            console.error(
    969              "Unknown operationTypeForDisplay: ",
    970              aResourceNameOrOperationType
    971            );
    972            return null;
    973          }
    974          if (bodyHasContent) {
    975            body = this.l10n.formatValueSync(bodyId, {
    976              agent: lazy.agentName,
    977              content: "",
    978            });
    979          } else {
    980            body = this.l10n.formatValueSync(bodyId);
    981          }
    982        }
    983        let alertBrowsingContext = aBrowsingContext;
    984        if (aBrowsingContext.embedderElement?.getAttribute("printpreview")) {
    985          // If we're in a print preview dialog, things are tricky.
    986          // The window itself is about to close (because of the thrown NS_ERROR_CONTENT_BLOCKED),
    987          // so using an async call would just immediately make the dialog disappear. (bug 1899714)
    988          // Using a blocking version can cause a hang if the window is resizing while
    989          // we show the dialog. (bug 1900798)
    990          // So instead, try to find the browser that this print preview dialog is on top of
    991          // and show the dialog there.
    992          let printPreviewBrowser = aBrowsingContext.embedderElement;
    993          let win = printPreviewBrowser.ownerGlobal;
    994          for (let browser of win.gBrowser.browsers) {
    995            if (
    996              win.PrintUtils.getPreviewBrowser(browser)?.browserId ===
    997              printPreviewBrowser.browserId
    998            ) {
    999              alertBrowsingContext = browser.browsingContext;
   1000              break;
   1001            }
   1002          }
   1003        }
   1004        await Services.prompt.asyncAlert(
   1005          alertBrowsingContext,
   1006          Ci.nsIPromptService.MODAL_TYPE_TAB,
   1007          this.l10n.formatValueSync(titleId),
   1008          body
   1009        );
   1010        return null;
   1011      }
   1012      case Ci.nsIContentAnalysisResponse.eUnspecified:
   1013        message = await this.l10n.formatValue(
   1014          "contentanalysis-unspecified-error-message-content",
   1015          {
   1016            agent: lazy.agentName,
   1017            content: this._getErrorDialogMessage(aResourceNameOrOperationType),
   1018          }
   1019        );
   1020        timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
   1021        break;
   1022      case Ci.nsIContentAnalysisResponse.eCanceled:
   1023        {
   1024          let messageId;
   1025          switch (aRequestCancelError) {
   1026            case Ci.nsIContentAnalysisResponse.eUserInitiated:
   1027              console.error(
   1028                "Got unexpected cancel response with eUserInitiated"
   1029              );
   1030              return null;
   1031            case Ci.nsIContentAnalysisResponse.eOtherRequestInGroupCancelled:
   1032              return null;
   1033            case Ci.nsIContentAnalysisResponse.eNoAgent:
   1034              messageId = "contentanalysis-no-agent-connected-message-content";
   1035              break;
   1036            case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
   1037              messageId =
   1038                "contentanalysis-invalid-agent-signature-message-content";
   1039              break;
   1040            case Ci.nsIContentAnalysisResponse.eErrorOther:
   1041              messageId = "contentanalysis-unspecified-error-message-content";
   1042              break;
   1043            case Ci.nsIContentAnalysisResponse.eShutdown:
   1044              // we're shutting down, no need to show a dialog
   1045              return null;
   1046            case Ci.nsIContentAnalysisResponse.eTimeout:
   1047              // We only show this if the default action was to block.
   1048              messageId = "contentanalysis-timeout-block-error-message-content";
   1049              break;
   1050            default:
   1051              console.error(
   1052                "Unexpected CA cancelError value: " + aRequestCancelError
   1053              );
   1054              messageId = "contentanalysis-unspecified-error-message-content";
   1055              break;
   1056          }
   1057          // We got an error with this request, so close any dialogs for any other request
   1058          // with the same user action id and also remove their data so we don't show
   1059          // any dialogs they might later try to show.
   1060          const busyDialogInfo =
   1061            this.userActionToBusyDialogMap.get(aUserActionId);
   1062          if (busyDialogInfo) {
   1063            busyDialogInfo.requestTokenSet.forEach(requestToken => {
   1064              this.requestTokenToRequestInfo.delete(requestToken);
   1065              this._removeSlowCAMessage(aUserActionId, requestToken);
   1066            });
   1067          }
   1068          message = await this.l10n.formatValue(messageId, {
   1069            agent: lazy.agentName,
   1070            content: this._getErrorDialogMessage(aResourceNameOrOperationType),
   1071            contentName: this._getResourceNameFromNameOrOperationType(
   1072              aResourceNameOrOperationType
   1073            ),
   1074          });
   1075          timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
   1076        }
   1077        break;
   1078      default:
   1079        throw new Error("Unexpected CA result value: " + aCAResult);
   1080    }
   1081 
   1082    if (!message) {
   1083      console.error(
   1084        "_showCAResult did not get a message populated for result value " +
   1085          aCAResult
   1086      );
   1087      return null;
   1088    }
   1089 
   1090    return this._showMessage(message, aBrowsingContext, timeoutMs);
   1091  },
   1092 
   1093  /**
   1094   * Returns the correct text for warn dialog contents.
   1095   *
   1096   * @param {ResourceNameOrOperationType} aResourceNameOrOperationType
   1097   */
   1098  async _warnDialogText(aResourceNameOrOperationType) {
   1099    const caInfo = await this.contentAnalysis.getDiagnosticInfo();
   1100    if (caInfo.connectedToAgent) {
   1101      return await this.l10n.formatValue("contentanalysis-warndialogtext", {
   1102        content: this._getResourceNameFromNameOrOperationType(
   1103          aResourceNameOrOperationType
   1104        ),
   1105      });
   1106    }
   1107    return await this.l10n.formatValue(
   1108      "contentanalysis-no-agent-connected-message-content",
   1109      { agent: lazy.agentName, content: "" }
   1110    );
   1111  },
   1112 };